LeetCode 49. 字母异位词分组 - 多语言解法详解
一、问题理解
「字母异位词分组」问题要求:给定一个字符串数组 strs,将字母异位词组合在一起,可以按任意顺序返回结果列表。字母异位词是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。
示例:
text
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"] 输出: [["bat"], ["nat", "tan"], ["ate", "eat", "tea"]]
其中 "eat", "tea", "ate" 是字母异位词,因为它们包含相同的字母,只是排列顺序不同。
二、核心思路
字母异位词的核心特征是:排序后会得到完全相同的字符串。因此,我们可以用「排序后的字符串」作为分组的 key,用「存储原字符串的列表」作为分组的 value,通过哈希表/字典实现快速分组。
算法步骤:
-
创建一个哈希表/字典,key 是排序后的字符串,value 是原字符串列表
-
遍历每个字符串:
-
将字符串转换为字符数组/列表并排序
-
将排序后的字符数组/列表转换为字符串作为 key
-
将原字符串添加到该 key 对应的列表中
-
-
返回哈希表/字典中所有值的集合
三、代码逐行解析
Java 解法
java
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// 1. 创建哈希表:key是排序后的字符串,value是同组原字符串的列表
// HashMap<String, List<String>> 表示键是字符串,值是字符串列表
Map<String, List<String>> m = new HashMap<>();
// 2. 遍历输入的每个字符串
for (String s : strs) {
// 2.1 将字符串转换为字符数组(方便排序)
// String 是不可变的,转换为 char[] 后才能排序
char[] sortedS = s.toCharArray();
// 2.2 对字符数组排序(异位词排序后会得到相同的字符序列)
// Arrays.sort() 使用双轴快速排序算法
Arrays.sort(sortedS);
// 2.3 核心:使用 computeIfAbsent 方法处理分组
// new String(sortedS):将排序后的字符数组转为字符串作为 key
// _ -> new ArrayList<>():如果 key 不存在,创建新的空列表
// .add(s):将原字符串 s 添加到列表中
m.computeIfAbsent(new String(sortedS), _ -> new ArrayList<>()).add(s);
}
// 3. 哈希表的 value 集合就是所有分组,转换为 List 返回
// m.values() 返回 Collection<List<String>>,用 ArrayList 包装
return new ArrayList<>(m.values());
}
}
Python 解法
python
from typing import List
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
# 1. 使用 defaultdict 创建字典,默认值为空列表
# 这样就不需要检查键是否存在,直接追加即可
anagram_dict = defaultdict(list)
# 2. 遍历输入的每个字符串
for s in strs:
# 2.1 将字符串排序作为 key
# ''.join(sorted(s)) 完成:将字符串 s 转换为列表并排序,再拼接回字符串
# 例如:"eat" -> sorted('eat') -> ['a', 'e', 't'] -> "aet"
key = ''.join(sorted(s))
# 2.2 将原字符串添加到对应 key 的列表中
# 由于使用了 defaultdict(list),如果 key 不存在会自动创建空列表
anagram_dict[key].append(s)
# 3. 返回字典中所有值的列表
# dict.values() 返回的是视图对象,需要用 list() 转换为列表
return list(anagram_dict.values())
四、Java 与 Python 语法对比
1. 哈希表/字典的创建与使用
| 操作 | Java | Python |
|---|---|---|
| 创建哈希表 | Map<String, List<String>> m = new HashMap<>(); | anagram_dict = {} 或 defaultdict(list) |
| 检查键是否存在 | m.containsKey(key) | key in anagram_dict |
| 获取值 | m.get(key) | anagram_dict[key] 或 anagram_dict.get(key) |
| 设置默认值 | m.computeIfAbsent(key, k -> new ArrayList<>()) | anagram_dict.setdefault(key, []) 或 defaultdict(list) |
| 获取所有值 | m.values() | anagram_dict.values() |
2. 字符串排序处理
| 操作 | Java | Python |
|---|---|---|
| 字符串转字符数组 | s.toCharArray() | list(s) |
| 排序字符数组 | Arrays.sort(sortedS) | sorted(s) |
| 字符数组转字符串 | new String(sortedS) | ''.join(sorted_chars) |
| 完整排序操作 | new String(Arrays.sort(s.toCharArray())) ❌ (错误,sort 返回 void) | ''.join(sorted(s)) ✅ |
3. 核心 API 对比
Java 的 computeIfAbsent 方法
java
// 语法:V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
// 作用:如果 key 不存在,使用 mappingFunction 创建值并存入,返回该值
// 如果 key 存在,直接返回对应的值
Map<String, List<String>> map = new HashMap<>();
// 传统写法
if (!map.containsKey(key)) {
map.put(key, new ArrayList<>());
}
map.get(key).add(value);
// 使用 computeIfAbsent 的简洁写法
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
Python 的等价操作
python
# 方法1:使用 setdefault
anagram_dict = {}
anagram_dict.setdefault(key, []).append(value)
# 方法2:使用 defaultdict(更简洁)
from collections import defaultdict
anagram_dict = defaultdict(list)
anagram_dict[key].append(value) # 如果 key 不存在会自动创建空列表
# 方法3:普通字典 + 判断
anagram_dict = {}
if key not in anagram_dict:
anagram_dict[key] = []
anagram_dict[key].append(value)
4. 返回结果处理
| 操作 | Java | Python |
|---|---|---|
| 获取哈希表所有值 | m.values() (返回 Collection<V>) | anagram_dict.values() (返回 dict_values 视图) |
| 转换为列表 | new ArrayList<>(m.values()) | list(anagram_dict.values()) |
| 列表嵌套结构 | List<List<String>> | List[List[str]] |
五、实例演示
以输入 strs = ["eat", "tea", "tan", "ate", "nat", "bat"] 为例:
Java 执行过程:
-
初始状态:
m = {}(空哈希表) -
处理 "eat":
-
sortedS = ['e','a','t']→ 排序 →['a','e','t'] -
key = new String(['a','e','t']) = "aet" -
m.computeIfAbsent("aet", ...)→ key 不存在,创建空列表[] -
添加 "eat" →
m = {"aet": ["eat"]}
-
-
处理 "tea":
-
sortedS = ['t','e','a']→ 排序 →['a','e','t'] -
key = "aet" -
m.computeIfAbsent("aet", ...)→ key 已存在,返回列表["eat"] -
添加 "tea" →
m = {"aet": ["eat", "tea"]}
-
-
处理 "tan":
-
sortedS = ['t','a','n']→ 排序 →['a','n','t'] -
key = "ant" -
添加 "tan" →
m = {"aet": ["eat", "tea"], "ant": ["tan"]}
-
-
继续处理剩余字符串...
-
最终返回:
[["eat", "tea", "ate"], ["tan", "nat"], ["bat"]](顺序可能不同)
Python 执行过程:
-
初始状态:
anagram_dict = defaultdict(list) -
处理 "eat":
-
key = ''.join(sorted("eat")) = "aet" -
anagram_dict["aet"].append("eat")→{"aet": ["eat"]}
-
-
处理 "tea":
-
key = ''.join(sorted("tea")) = "aet" -
anagram_dict["aet"].append("tea")→{"aet": ["eat", "tea"]}
-
-
处理 "tan":
-
key = ''.join(sorted("tan")) = "ant" -
anagram_dict["ant"].append("tan")→{"aet": ["eat", "tea"], "ant": ["tan"]}
-
-
继续处理剩余字符串...
-
最终返回:
list(anagram_dict.values())
六、复杂度分析
时间复杂度
-
Java: O(n * k log k)
-
n是字符串数组的长度 -
k是字符串的最大长度 -
对每个字符串排序:O(k log k)
-
哈希表操作:O(1) 平均时间复杂度
-
-
Python: O(n * k log k)
-
与 Java 相同,
sorted()函数使用 Timsort 算法,时间复杂度为 O(k log k)
-
空间复杂度
-
Java: O(n * k)
-
哈希表存储所有字符串
-
排序需要 O(k) 的额外空间(字符数组)
-
-
Python: O(n * k)
-
字典存储所有字符串
-
sorted()创建新的列表,需要 O(k) 额外空间
-
七、优化思路与变体解法
1. 计数法(避免排序)
使用字符计数作为 key,而不是排序后的字符串:
python
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
anagram_dict = defaultdict(list)
for s in strs:
# 创建长度为26的计数数组(只包含小写字母)
count = [0] * 26
for char in s:
count[ord(char) - ord('a')] += 1
# 将计数数组转换为元组作为key(元组是可哈希的)
key = tuple(count)
anagram_dict[key].append(s)
return list(anagram_dict.values())
复杂度分析:
-
时间复杂度:O(n * k),其中 k 是字符串长度(避免了排序的 O(k log k))
-
空间复杂度:O(n * k)
2. Java 计数法实现
java
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String s : strs) {
int[] count = new int[26];
for (char c : s.toCharArray()) {
count[c - 'a']++;
}
// 将计数数组转换为字符串作为key,如 "#1#2#0#3..."
StringBuilder keyBuilder = new StringBuilder();
for (int num : count) {
keyBuilder.append('#').append(num);
}
String key = keyBuilder.toString();
map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(map.values());
}
}
八、常用函数积累
Java 常用函数
java
// 字符串操作 String s = "hello"; char[] chars = s.toCharArray(); // 字符串转字符数组 String sortedStr = new String(chars); // 字符数组转字符串 Arrays.sort(chars); // 数组排序 // 哈希表操作 Map<String, List<String>> map = new HashMap<>(); map.computeIfAbsent(key, k -> new ArrayList<>()); // 如果不存在则创建 map.getOrDefault(key, defaultValue); // 获取值或默认值 map.containsKey(key); // 检查键是否存在 map.values(); // 获取所有值
Python 常用函数
python
# 字符串操作
s = "hello"
chars = list(s) # 字符串转列表
sorted_str = ''.join(sorted(s)) # 排序并拼接回字符串
sorted_chars = sorted(s) # 返回排序后的列表
# 字典操作
from collections import defaultdict
d = defaultdict(list) # 默认值为空列表的字典
d[key].append(value) # 自动处理不存在的键
d = {}
d.setdefault(key, []).append(value) # 设置默认值并追加
# 字符计数
from collections import Counter
counter = Counter(s) # 统计字符出现次数
九、总结
核心要点
-
字母异位词的本质:排序后相同的字符串是字母异位词
-
哈希表/字典是关键:用排序后的字符串作为 key,原字符串列表作为 value
-
多语言实现差异:
-
Java 使用
computeIfAbsent简化「检查键是否存在→创建默认值」的操作 -
Python 使用
defaultdict或setdefault实现类似功能
-
-
优化思路:使用字符计数法可以避免排序,将时间复杂度从 O(n * k log k) 优化到 O(n * k)
面试常见问题
-
为什么排序后的字符串可以作为 key?
-
因为字母异位词排序后得到相同的字符串
-
-
为什么不能直接用字符数组作为 key?
-
数组的
equals()方法是引用比较,而字符串的equals()是内容比较 -
在 Python 中,列表不能作为字典的 key(不可哈希),而元组可以
-
-
如何处理大小写敏感的情况?
-
可以在排序前统一转换为小写:
s.toLowerCase()(Java) 或s.lower()(Python)
-
-
如果字符串包含 Unicode 字符怎么办?
-
使用字符计数法更通用,排序法可能受字符编码影响
-
掌握这种分组问题的解法,不仅有助于解决字母异位词分组,还可以应用于其他需要根据某种规则对元素进行分组的问题,是面试中的高频考点。

1911






