NO.49 字母异位词分组——LeetCode热题100解析

10 阅读5分钟

NO.49 字母异位词分组——LeetCode热题100解析

一、标准解题逻辑步骤

核心思路

字母异位词 的特点是:排序后的字符串相同。例如 "eat""tea""ate" 排序后都是 "aet"

解题步骤:

  1. 创建一个哈希表(Map),键为排序后的字符串,值为原字符串组成的数组
  2. 遍历输入数组中的每个字符串
  3. 对当前字符串进行排序,得到唯一标识 key
  4. 检查 Map 中是否已有该 key:
    • 如果没有,创建一个空数组作为 value
    • 如果有,直接获取该数组
  5. 将原字符串 push 到对应的数组中
  6. 遍历结束后,返回 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()]

三、得分重点与理解难点

得分重点

  1. 识别字母异位词的关键特征:排序后相同
  2. 选择合适的哈希表数据结构:Map 或对象
  3. 字符串处理的基本操作:拆分 → 排序 → 重组

理解难点

  1. 为什么排序能解决字母异位词问题?
    • 字母异位词只是字符顺序不同,排序后顺序一致
  2. 时间复杂度分析
    • 每个字符串排序:O(k log k),k 为字符串长度
    • 总体:O(n * k log k),n 为字符串数量
  3. 空间换时间思想
    • 用哈希表存储中间结果,避免重复比较

四、极易踩错的细节和边界错误

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) = 97
  • char.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)
代码简洁度简洁稍复杂
适用场景字符串长度较小字符串长度较大
推荐程度面试够用更优解法

面试建议

  1. 先说排序法(思路清晰)
  2. 再提计数法优化(展示深度)
  3. 注意边界:空字符串、单字符字符串
  4. 说明 Map 比普通对象的优势(键类型灵活、避免原型链污染)