一、问题背景
字母异位词:由相同字母组成、但字母排列顺序不同的单词(如 "eat"、"tea"、"ate")。
目标:将数组中所有互为字母异位词的单词分组到一起。
示例:
- 输入:
["eat","tea","tan","ate","nat","bat"] - 输出:
[["bat"],["nat","tan"],["ate","eat","tea"]]
二、解法一:排序字符串作为键(推荐)
核心思路
- 互为异位词的字符串,排序后结果完全相同(如
"eat"和"tea"排序后均为"aet")。 - 用排序后的字符串作为
key,原字符串作为value,存入字典分组。 - 时间复杂度:O(n * k log k) (n 为单词总数,k 为单词最大长度)
- 空间复杂度:O(n * k)
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: list[str]) -> list[list[str]]:
# 增强版字典:当访问不存在的键时,自动创建空列表作为默认值
d = defaultdict(list)
for s in strs:
# 对字符串排序,生成唯一key
sorted_str = "".join(sorted(s))
# 将原字符串加入对应分组
d[sorted_str].append(s)
# 取出字典中所有值,即为分组结果
return list(d.values())
关键知识点
defaultdict(list):避免手动判断key是否存在,直接调用append()。sorted(s):对任意序列排序,返回列表;"".join(...)将列表拼接为字符串。- 示例:
sorted("tea") → ['t','e','a']→"aet"。
三、解法二:字符计数作为键(进阶)
核心思路
- 字母仅为小写(26 个),用长度为 26 的数组统计每个字母出现次数。
- 将计数数组转换为元组(可哈希)作为
key,原字符串作为value分组。 - 时间复杂度:O(n * k) (避免排序,计数为线性操作)
- 空间复杂度:O(n * k)
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: list[str]) -> list[list[str]]:
# 创建一个默认值为列表的字典
anagram_map = defaultdict(list)
for s in strs:
# 初始化26个0的计数数组,对应a-z
count = [0] * 26
for char in s:
# 将字符映射到0-25的索引,并计数
count[ord(char) - ord('a')] += 1
# 列表不可哈希,转换为元组作为key
anagram_map[tuple(count)].append(s)
# 返回所有分组
return list(anagram_map.values())
关键知识点
ord(char) - ord('a'):将字符a-z映射到0-25的索引。- 元组
tuple(count):列表不可作为字典键,元组可哈希,适合作为key。 - 优势:无需排序,在单词较长时性能更优。
四、核心 API 与技巧总结
| 函数 / 类 | 作用 |
|---|---|
defaultdict(list) | 自动创建空列表作为默认值,简化分组逻辑 |
sorted(s) | 对字符串排序,生成异位词的唯一标识 |
ord(c) | 返回字符的 Unicode 编码,用于字符计数 |
tuple(list) | 将列表转为可哈希的元组,作为字典键 |
dict.values() | 取出字典中所有值,得到最终分组 |
五、两种解法对比
| 维度 | 排序法 | 计数法 |
|---|---|---|
| 实现复杂度 | 简单,代码更短 | 稍复杂,需手动计数 |
| 时间效率 | O(nk log k) | O (nk),长文本更优 |
| 适用场景 | 通用场景,代码易读 | 已知字符范围(如小写字母) |
| 空间占用 | O(nk) | O(nk) |