这道题在 LeetCode 里不算难,但非常值得认真写一篇笔记。
因为它真正考的不是 API,而是一句话:
你能不能找到“一组字符串的共同特征”,并把它当成 key?
一、什么是字母异位词?
先把概念说清楚。
如果两个字符串:
- 含有的字符完全相同
- 每个字符出现的次数也相同
- 只是顺序不同
那么它们就是 字母异位词。
例子:
"eat"、"tea"、"ate" → 同一组
"tan"、"nat" → 同一组
二、最关键的问题:怎么判断“是不是一组”?
这是整道题的灵魂。
我们需要一个统一的、可比较的特征,让:
- 同一组的字符串 → 特征相同
- 不同组的字符串 → 特征不同
一个非常自然的想法
把字符串里的字符排序
举个例子:
"eat" → ['e','a','t'] → 排序 → "aet"
"tea" → ['t','e','a'] → 排序 → "aet"
不管原来顺序如何,只要是字母异位词,
排序后的结果一定一样。
这个排序后的字符串,就是我们要找的 key。
三、整体思路(先看大框架)
-
遍历字符串数组
-
对每个字符串:
- 转成字符数组
- 排序
- 作为 HashMap 的 key
-
相同 key 的字符串,放进同一个 List
-
最后取出 map 中所有 value 即可
一句话总结:
HashMap<String, List>,key 是“排序后的字符串”
四、代码实现
完整代码
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) { // 1. 遍历字符串数组
char[] chs = str.toCharArray(); // 2. 字符串 → 字符数组
Arrays.sort(chs); // 3. 排序
String key = new String(chs); // 4. 排序后的字符串作为 key
if (map.containsKey(key)) {
map.get(key).add(str); // 5. 已存在:直接加入
} else {
List<String> list = new ArrayList<>();
list.add(str);
map.put(key, list); // 6. 不存在:新建分组
}
}
// 7. 直接取出所有分组
return new ArrayList<>(map.values());
}
}
五、为什么 HashMap 这么合适?
因为这道题本质是:
分类问题
而 HashMap 天生就是用来:
- 用一个 key
- 对应一组 value
这里:
- key:排序后的字符串(分组依据)
- value:所有属于这一组的原字符串
这比用双重循环去硬比较,清晰太多了。
六、一个容易被忽略的小细节
String key = new String(chs);
为什么不能直接用 char[] 当 key?
因为:
- 数组在 Java 中是引用类型
- 就算内容一样,地址不同,HashMap 也认为是不同 key
而 String:
- 是不可变对象
- 重写了
equals和hashCode - 非常适合作为 HashMap 的 key
七、时间 & 空间复杂度分析
设:
- n = 字符串个数
- k = 每个字符串的平均长度
时间复杂度
- 每个字符串排序:
O(k log k) - 总体:
O(n * k log k)
空间复杂度
- HashMap 存储所有字符串
- 排序时使用字符数组
- 总体:
O(n * k)
八、这道题还能怎么优化?
如果字符集固定(比如只包含 a-z),可以用:
- 字符频次数组
- 再把频次数组编码成字符串当 key
时间复杂度可以降到 O(n * k),
但实现复杂度更高,不是这道题的重点。
九、小总结
这道题真正想教我们的不是排序,也不是 HashMap API,而是:
- 如何从题目中 抽象“分组依据”
- 如何把“相同特征”转化成 可比较的 key
- 如何用 HashMap 把复杂问题结构化解决
很多字符串题,
本质都是:找 key → 分组 → 处理 value。