LeetCode 49中等哈希分组
字母异位词分组 图解题解
字母一样、顺序乱的词如何自动归堆?给每个词做一个「排序身份证」,哈希表帮你一步到位。
想象你是图书馆管理员,手里有一堆书,书名顺序各异但用字相同。与其两两对比书名,不如把每本书的书名字母排序后当「分类码」,贴到书脊上——分类码一样的直接推进同一个架格。哈希表就是这排书架,同一个 key 的词自动落进同一个桶,不用逐组比对。
这道题到底在问什么
把字母相同、顺序不同的字符串分到同一组。
- strs
- ["eat","tea","tan","ate","nat","bat"]
- 输出
- [[eat,tea,ate],[tan,nat],[bat]]
先想最直接的笨办法
暴力做法是每来一个词,都和已有组逐个比较。比较前还要排序,组一多就慢。(动画第 3 步)
最优解:一步一步想明白
- 3暴力做法是每来一个词,都和已有组逐个比较。比较前还要排序,组一多就慢。
- 4把每个词排序后当 key。key 一样,直接塞进同一个桶,哈希表自动帮我们聚合。
- 5e / a / t异位词不是直接比较原串,而是先把字符拆开,准备归一化。
- 6key=aet排序后得到 key=aet。这个 key 才是进桶的身份证。
- 7new bucket哈希表里还没有 aet,就新建一个桶,把 eat 放进去。
- 8key=aettea 的字母顺序不同,但排序后仍然是 aet,所以它应该回到同一个桶。
- 9hit aetkey 命中已有桶,tea 不和 eat 逐字符比较,直接加入 aet 组。
- 10key=anttan 排序后是 ant,和 aet 不一样。新 key,得开新桶。
- 11new bucket ant哈希表里没有 ant,新建第二个桶,把 tan 放进去。
- 12key=aetate 字母顺序又不同,排序后还是 aet,应该回到第一个桶。
- 13same key same bucketate 排序后还是 aet,命中第一个桶。每个词都先排序再归桶,没有凭空分组。
- 16key 设计对了,分组逻辑就只剩下塞桶。
- 19wrong key如果直接用原字符串,eat 和 tea 会落到两个桶,分组立刻错。
- 26这题学完不要乱跳,下一步去 对应专题 连刷同类题;卡住时用 AI 答疑追问“为什么这一步成立”,比只背答案更稳。
⚠️ 容易写错的地方
✗ 错:直接用原字符串当 key
✓ 对:用排序串或 26 位计数元组
eat 和 tea 原串不同,但归一化 key 相同。
✗ 错:计数数组直接当 dict key
✓ 对:转成 tuple
Python list 不可哈希。
✗ 错:普通 dict 直接 append
✓ 对:用 defaultdict 或 computeIfAbsent
桶不存在时要先建桶。
完整代码(Python / C++ / Java)
Python
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs):
groups = defaultdict(list)
for s in strs:
key = ''.join(sorted(s))
groups[key].append(s)
return list(groups.values())C++
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> groups;
for (string s : strs) {
string key = s;
sort(key.begin(), key.end());
groups[key].push_back(s);
}
vector<vector<string>> ans;
for (auto& [key, bucket] : groups) ans.push_back(bucket);
return ans;
}
};Java
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> groups = new HashMap<>();
for (String s : strs) {
char[] arr = s.toCharArray();
Arrays.sort(arr);
String key = new String(arr);
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(groups.values());
}
}套路模板 · 哈希归一化分组迁移骨架
# 哈希分组三件套:归一化 key → 取桶 → 塞元素
groups = defaultdict(list)
for x in 输入:
key = 归一化(x) # 排序串 / 26位计数元组 / 数值本身
groups[key].append(x) # 桶不存在 defaultdict 自动建
return list(groups.values())unordered_map<Key, vector<Item>> groups;
for (auto& x : input) {
Key key = normalize(x); // 排序串 / 计数签名
groups[key].push_back(x); // [] 自动建空桶
}
// 收集 groups 的所有 value 即答案Map<Key, List<Item>> groups = new HashMap<>();
for (Item x : input) {
Key key = normalize(x); // 排序串 / 计数签名
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(x);
}
// new ArrayList<>(groups.values()) 即答案复杂度
时间复杂度
O(n·klogk)
n 个词,每个词排序 klogk
空间复杂度
O(n·k)
哈希表存下所有字符串
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 字母异位词分组 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么排序串能当 key?+
异位词含相同字符,多重集合相同;排序后必然得到同一个字符串。
这道题为什么用「哈希分组」,换最直接的暴力解会差在哪?+
哈希分组抓住了本题的结构特征,把暴力解里重复的工作省掉;暴力解通常要多嵌套一层枚举,数据一大就超时。具体对比见上文「暴力解及其卡点」与「最优解逐步推演」两节。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。