NO.49 字母异位词分组——LeetCode热题100解析
一、标准解题逻辑步骤
核心思路
字母异位词 的特点是:排序后的字符串相同。例如 "eat"、"tea"、"ate" 排序后都是 "aet"。
解题步骤:
- 创建一个哈希表(Map),键为排序后的字符串,值为原字符串组成的数组
- 遍历输入数组中的每个字符串
- 对当前字符串进行排序,得到唯一标识 key
- 检查 Map 中是否已有该 key:
- 如果没有,创建一个空数组作为 value
- 如果有,直接获取该数组
- 将原字符串 push 到对应的数组中
- 遍历结束后,返回 Map 中所有的 value 组成的数组
二、代码中每个 API 的详细解析
1. str.split('')
- 作用:将字符串按指定分隔符拆分成数组
- 参数:
''(空字符串)表示按每个字符拆分 - 返回值:字符数组
- 示例:
"eat".split('')→['e', 'a', 't'] - 适用场景:需要逐个处理字符串中每个字符时
2. Array.sort()
- 作用:对数组元素进行排序(默认按 UTF-16 代码单元值排序)
- 参数:可选,比较函数。不传参时按字符串顺序排序
- 返回值:排序后的原数组(会改变原数组)
- 示例:
['e', 'a', 't'].sort()→['a', 'e', 't'] - 注意:因为字母异位词只包含小写字母,默认排序即可
3. Array.join('')
- 作用:将数组所有元素连接成一个字符串
- 参数:
''(空字符串)表示元素之间直接相连,不加分隔符 - 返回值:连接后的字符串
- 示例:
['a', 'e', 't'].join('')→"aet"
4. new Map()
- 作用:创建一个 Map 对象,存储键值对,键可以是任意类型
- 特点:比普通对象更适合此场景,因为键是字符串且有序
5. map.has(key)
- 作用:判断 Map 中是否存在指定的键
- 参数:要检查的键
- 返回值:布尔值
true/false
6. map.set(key, value)
- 作用:向 Map 中添加或更新键值对
- 参数:key(键)、value(值)
- 返回值:Map 对象本身(支持链式调用)
7. map.get(key)
- 作用:获取 Map 中指定键对应的值
- 参数:要获取的键
- 返回值:键对应的值,不存在则返回
undefined
8. Array.from(map.values())
- 作用:将可迭代对象(如 Map 的 values)转换成数组
- 参数:
map.values()返回键值对中所有 value 组成的迭代器 - 返回值:新数组
- 替代写法:
[...map.values()]
三、得分重点与理解难点
得分重点
- 识别字母异位词的关键特征:排序后相同
- 选择合适的哈希表数据结构:Map 或对象
- 字符串处理的基本操作:拆分 → 排序 → 重组
理解难点
- 为什么排序能解决字母异位词问题?
- 字母异位词只是字符顺序不同,排序后顺序一致
- 时间复杂度分析
- 每个字符串排序:
O(k log k),k 为字符串长度 - 总体:
O(n * k log k),n 为字符串数量
- 每个字符串排序:
- 空间换时间思想
- 用哈希表存储中间结果,避免重复比较
四、极易踩错的细节和边界错误
1. 空字符串处理
// 空字符串 "" 没问题
"".split('') → []
[].sort() → []
[].join('') → "" // key 为空字符串
2. 原数组被 sort 方法改变
// 错误写法:会影响原字符串吗?
str.split('').sort() // sort 改变的是数组,不是原字符串
// str 本身不会被改变,因为字符串是不可变的
3. 忘记初始化数组
// 错误
if (!map.has(key)) {
map.set(key, str); // 应该存数组,不是字符串
}
// 正确
if (!map.has(key)) {
map.set(key, []);
}
4. 直接返回 map 而不是 values
// 错误
return map; // 返回 Map 对象,不符合题目要求
// 正确
return Array.from(map.values());
5. 使用普通对象代替 Map 时的陷阱
// 普通对象写法
const map = {};
const key = "aet";
if (!map[key]) map[key] = [];
map[key].push(str);
return Object.values(map); // 需要用 Object.values()
6. 性能陷阱:在大数据量下频繁字符串操作
- 每个字符串都要 split、sort、join
- 优化:可以用计数法(见下文优化方案)
五、优化思路与代码
方案一:当前解法(标准写法)
var groupAnagrams = function(strs) {
const map = new Map();
for (const str of strs) {
const key = str.split('').sort().join('');
if (!map.has(key)) map.set(key, []);
map.get(key).push(str);
}
return Array.from(map.values());
};
- 时间复杂度:
O(n * k log k) - 空间复杂度:
O(n * k)
方案二:计数法(更优,避免排序)
用 26 个字母的计数数组作为 key,避免排序的 O(k log k) 开销
var groupAnagrams = function(strs) {
const map = new Map();
for (const str of strs) {
// 创建长度为 26 的计数数组
const count = new Array(26).fill(0);
// 统计每个字母出现次数
for (const char of str) {
count[char.charCodeAt(0) - 97]++;
}
// 将计数数组转为字符串作为 key
const key = count.join('#');
if (!map.has(key)) map.set(key, []);
map.get(key).push(str);
}
return Array.from(map.values());
};
优势:
- 时间复杂度:
O(n * k),k 为字符串长度 - 空间复杂度:
O(n * k),但 key 长度为 26,更稳定 - 避免了字符串排序,对大字符串更友好
字符编码说明:
charCodeAt(0):获取字符的 Unicode 编码'a'.charCodeAt(0)= 97char.charCodeAt(0) - 97:将 a-z 映射到 0-25 的索引
方案三:使用对象键(更简洁但可能有性能问题)
var groupAnagrams = function(strs) {
const map = {};
for (const str of strs) {
const key = str.split('').sort().join('');
map[key] = map[key] ? [...map[key], str] : [str];
}
return Object.values(map);
};
- 注意:扩展运算符
...会创建新数组,效率略低于 push
六、总结
| 对比项 | 排序法 | 计数法 |
|---|---|---|
| 时间复杂度 | O(n × k log k) | O(n × k) |
| 代码简洁度 | 简洁 | 稍复杂 |
| 适用场景 | 字符串长度较小 | 字符串长度较大 |
| 推荐程度 | 面试够用 | 更优解法 |
面试建议:
- 先说排序法(思路清晰)
- 再提计数法优化(展示深度)
- 注意边界:空字符串、单字符字符串
- 说明 Map 比普通对象的优势(键类型灵活、避免原型链污染)