49. 字母异位词分组(Group Anagrams)

难度:中等 | 主题:哈希表

题目

给你一个字符串数组,请你将字母异位词(由重新排列源单词的所有字母得到的一个新单词)组合在一起。可以按任意顺序返回结果列表。

示例

 
输入:strs = ["eat","tea","tan","ate","nat","bat"]
 
输出:[["bat"],["nat","tan"],["ate","eat","tea"]]
 

思路

先讲个故事:快递分拣站

你是快递分拣站的工人。传送带上有许多包裹,每个包裹上写着一个单词。

你的任务:把由相同字母组成的单词分到同一个筐里——不管字母顺序怎么打乱。

比如 “eat”、“tea”、“ate”——都是 e、a、t 三个字母——放同一个筐。“tan” 和 “nat”——t、a、n——另一个筐。“bat” 独自一筐。

你怎么快速判断哪些单词是同一类?把每个单词的字母按字母表排好序。排完序后一样的就是同一类:

 
eat → 排序 → aet
 
tea → 排序 → aet  ✓ 同类
 
tan → 排序 → ant
 
nat → 排序 → ant  ✓ 同类
 
bat → 排序 → abt  ✗ 不同类
 

对每个单词,排序后的结果就是它的识别码。识别码相同的放一个筐。


引导式推导:从识别码到哈希

第 1 步:什么是异位词?

两个单词包含完全相同的字符,只是顺序不同。比如 “ate” 和 “eat”。

第 2 步:找统一标志

要给每组异位词找一个唯一标志。两个候选:

方法标志举例
排序法排序后字符串”ate”,“eat”,“tea” → “aet”
计数法26 位频次编码”ate” → “1#0#0#0#1#0#…”

第 3 步:用哈希表分组


graph LR

    A["遍历 strs<br/>s = 'eat'"] --> B["key = ''.join(sorted(s))<br/>→ 'aet'"]

    B --> C["groups[key].append(s)<br/>groups['aet'] = ['eat']"]

    C --> D["下一个 s"]

    D --> A

    A -->|"所有单词遍历完毕"| E["return list(groups.values())"]

核心逻辑:同一个 key 的所有单词 → 同一组


两层递进


graph LR

    A["排序法<br/>O(n·k·log k)"]

    B["计数法<br/>O(n·k)"]

    A -->|"k 很大时优化"| B

排序法(推荐先写)''.join(sorted(s)) 做 key。简洁明了,k ≤ 100 时足够快。

计数法(追问优化时提):用 26 位计数数组拼成字符串做 key。如 [1,0,0,0,1,0,...]"1#0#0#0#1#..."。O(n·k),当单词长度 k 很大时更快。但代码更啰嗦,先写排序法。


代码

 
from collections import defaultdict
 
def groupAnagrams(self, strs):
 
    groups = defaultdict(list)        # key 不存在时自动创建空列表
 
    for s in strs:
 
        key = ''.join(sorted(s))     # 排序后做 key,异位词 key 相同
 
        groups[key].append(s)        # 加入对应分组
 
    return list(groups.values())     # 返回所有分组的列表
 

复杂度

方法时间空间
排序法O(n·k·log k)O(n·k)
计数法O(n·k)O(n·k)

n = 单词数量,k = 单词最大长度


实战考量

频率分析

出现在:字节/阿里/美团一面,哈希表应用的经典题。考察你为复杂对象(字符串)设计 key 的能力。

延伸思考

Q:为什么用 defaultdict(list) 不用普通 dict?

A:少写三行 if key not in groups: groups[key] = []。群面时一眼看起来更干净。

Q:如果字符串很长,排序太慢怎么办?

A:用计数法。26 位计数数组 [0]*26,遍历字符串统计每个字母出现次数,转成 "#".join(map(str, count)) 做 key。时间降到 O(n·k)。

Q:「242有效的字母异位词」和这题什么关系?

A:242 是基础版——只判断两个字符串是否异位词,用一个 26 位计数器就行。这题是进阶版——需要把大量字符串分组,用哈希表维护多组。

Q:能不能不用排序也不计数?

A:可以用质数映射——给每个字母分配一个质数,单词的乘积作为 key。异位词的质数乘积相同。但乘积可能溢出大整数,不推荐。

易错点

  • sorted(s) 返回列表,必须用 ''.join() 转字符串才能做 key

  • 字符串没有 .sort() 方法,只能用 sorted(s)

  • defaultdict(list) 比普通 dict 省代码


生活类比

字母异位词分组 → 快递分拣站 → 看排序后的样子

就像传送带上的包裹,不看表面标签,而是看里面的东西排好序后长什么样。

一样的放一起。不用纠结每个单词的字母顺序——排完序都一样。

核心就是:找到每个对象的「规范形」,相同的归一类。


相关题目

题目关系
242有效的字母异位词同族基础版,判断两个字符串是否异位词

→ 返回题单:LeetCode学习路线图 > 一、数组与哈希(含双指针、矩阵)

速记卡(面试闪卡)

Q1:一句话讲清「49. 字母异位词分组(Group Anagrams)」到底是什么?

A:《字母异位词分组》是按字母组成把字符串分组,靠设计 key 用哈希表归类。

Q2:题目 —— 怎么理解?

A:像把同款包裹归筐:给你字符串数组,把字母异位词(字母相同顺序不同)组合在一起;题目(Problem)要的是分组结果。

Q3:思路 —— 怎么理解?

A:像快递分拣站:把每个单词字母按字母表排好序,排序结果相同的就是同一类;识别码(Key)相同即同筐,典型 key 设计题。

Q4:代码 —— 怎么理解?

A:哈希表 defaultdict(list):对每词 sorted(s) 排序做 key,append 进对应组;代码(Code)一行 key 即可,计数法在 k 大时更优。

Q5:复杂度 —— 怎么理解?

A:排序法时间 O(n·k·log k)、空间 O(n·k);计数法 O(n·k) 更快但啰嗦。复杂度(Complexity)随单词数 n 与长度 k 变化。

Q6:核心速记主线有哪些?

  • 题目:异位词(同字母不同序)归为一组

  • 思路:排序或计数生成 key,哈希分组

  • 代码:defaultdict(list) 加 sorted(s) 做 key

  • 复杂度:排序法 O(n·k·log k),计数法 O(n·k)

口诀

A:异位词怎么分,

排序做key同筐进;

快递分拣看规范,

相同字母归一群。

相关链接