题目描述
思路解析
一句话答案:LeetCode 49 字母异位词分组的通用解法是排序键加哈希表:把每个单词的字母排序得到规范 key,互为异位词的单词排序后必然相同,用哈希表把相同 key 的单词收进同一组,一次遍历完成分组,时间 O(n·k·log k)、空间 O(n·k);用 26 字母计数当 key 还能进一步降到 O(n·k)。
这道题真正在问什么
题目给一组单词,要求把「字母完全相同、只是排列顺序不同」的单词归到同一组,返回全部分组,例如 eat、tea、ate 是一组。它本质不是在问「两个词是不是异位词」,而是在问「怎么给 n 个单词按异位关系划分等价类」——想清楚这一点,就能看出两两比较的思路从一开始就走偏了。
为什么不能两两比较判断异位词
最直觉的做法是拿每对单词比一比,统计两边字母出现次数是否一致。判断一对是 O(k)(k 为单词长度),但 n 个单词约有 n²/2 对,整体 O(n²·k);而且比完还得靠并查集之类的结构才能落成分组,又慢又绕。
换个角度想:分组问题的通用套路是给每个元素算一个「指纹」——同组指纹必然相同、不同组必然不同,然后按指纹归堆。只要指纹算得快,分组就退化成一次哈希表遍历。于是问题变成:什么指纹能唯一刻画「互为异位词」这层等价关系?
为什么排序后的字符串能当 key
两个单词互为异位词,等价于「每个字母出现的次数完全相同」。把字母排序恰好抹掉顺序差异、只保留字母及其个数:eat、tea、ate 排序后都是 aet;反过来,排序结果相同也必然意味着每种字母个数相同。所以「排序后的字母序列」与「互为异位词」是充要条件,拿它当哈希表的 key 既不会错分也不会漏分。注意不能偷懒成「字母集合」——aab 和 ab 的字母集合相同,但出现次数不同,并不是异位词。
哈希表怎么一次遍历完成分组
建一张表,key 是排序后的字母序列,value 是这一组单词的列表。从左到右扫一遍:每个单词先排序算出 key,表里没有这个 key 就新建一组放进去,有就追加到已有的组。这里的关键动作是「追加」而不是「赋值」——Python 写成 groups.setdefault(key, []).append(w),如果直接对 key 赋新列表,会把同组先来的单词整组覆盖丢掉。扫完把表里所有的组收集起来就是答案。
复杂度怎么算,还能更快吗
时间 O(n·k·log k):n 个单词各排序一次,每次 O(k·log k),哈希插入均摊 O(1)。空间 O(n·k):哈希表要存下全部单词。
想去掉排序的 log,可以换一种指纹:统计 26 个字母各出现几次,拼成形如 a2b1 的计数串当 key,构造一个 key 只要 O(k),整体降到 O(n·k)。两种 key 都正确:排序版更好写,计数版在单词较长时更快,面试里说清这层取舍就是加分项。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这条:排序后字母序列相同的单词,就是同一组异位词。
上面是单词数组(下标固定)。分组表一开始是空的,从左到右逐个处理每个单词。
处理单词 'eat':把它的字母排序,得到 key = "aet"。等会儿就拿这个 key 去分组表里找它该归哪一组。
分组表里还没有 key "aet",说明 'eat' 是这一组的第一个单词,新建一组把它放进去。
处理单词 'tea':把它的字母排序,得到 key = "aet"。等会儿就拿这个 key 去分组表里找它该归哪一组。
分组表里已经有 key "aet" 了,说明前面出现过它的异位词,把 'tea' 追加到同一组。
处理单词 'tan':把它的字母排序,得到 key = "ant"。等会儿就拿这个 key 去分组表里找它该归哪一组。
分组表里还没有 key "ant",说明 'tan' 是这一组的第一个单词,新建一组把它放进去。
处理单词 'ate':把它的字母排序,得到 key = "aet"。等会儿就拿这个 key 去分组表里找它该归哪一组。
分组表里已经有 key "aet" 了,说明前面出现过它的异位词,把 'ate' 追加到同一组。
处理单词 'nat':把它的字母排序,得到 key = "ant"。等会儿就拿这个 key 去分组表里找它该归哪一组。
分组表里已经有 key "ant" 了,说明前面出现过它的异位词,把 'nat' 追加到同一组。
处理单词 'bat':把它的字母排序,得到 key = "abt"。等会儿就拿这个 key 去分组表里找它该归哪一组。
分组表里还没有 key "abt",说明 'bat' 是这一组的第一个单词,新建一组把它放进去。
处理单词 'tab':把它的字母排序,得到 key = "abt"。等会儿就拿这个 key 去分组表里找它该归哪一组。
分组表里已经有 key "abt" 了,说明前面出现过它的异位词,把 'tab' 追加到同一组。
处理单词 'ant':把它的字母排序,得到 key = "ant"。等会儿就拿这个 key 去分组表里找它该归哪一组。
分组表里已经有 key "ant" 了,说明前面出现过它的异位词,把 'ant' 追加到同一组。
处理单词 'abt':把它的字母排序,得到 key = "abt"。等会儿就拿这个 key 去分组表里找它该归哪一组。
分组表里已经有 key "abt" 了,说明前面出现过它的异位词,把 'abt' 追加到同一组。
全部处理完,分组表里有 3 个 key,就是 3 组异位词——把每个 key 对应的单词列表收集起来就是答案。
边界先想清:空串、单词、全异位词。
两个高频追问。
参考代码
def groupAnagrams(strs): groups = {} # key -> 单词组 for w in strs: key = "".join(sorted(w)) # 排序后的字母序列 groups.setdefault(key, []).append(w) return list(groups.values())复杂度
- 时间:O(n·k·log k),n 个单词,每个长 k,排序 k·log k
- 空间:O(n·k),哈希表存下所有单词
易错点
面试追问把动画讲成自己的话
追问除了排序,还能用什么当 key?
追问为什么相同 key 一定互为异位词?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
前 K 个高频元素
LeetCode 347 · 中等 · 沿着 数组 & 哈希 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题