LeetCode 49. 字母异位词分组:经典哈希解法解析+易错点规避

「字母异位词」是字符串类题目中极具代表性的考点------从简单的 LeetCode 242. 有效的字母异位词 判断,到这道中等难度的 49. 字母异位词分组,核心思想一脉相承,但更侧重"分组逻辑"和"代码严谨性"。本文将聚焦 49 题,逐行拆解给定的经典哈希解法,剖析核心思路、指出高频易错点,再补充优化方向,帮你吃透这道面试高频题,做到"会写、会避坑、会优化"。

一、题目核心解读

题目描述:给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

核心重申:字母异位词,指由相同字母按照不同顺序组成的字符串(如 "eat"、"tea"、"ate" 是一组异位词,"bat" 单独为一组)。

关键要求(隐含坑点):

  • 返回值要求:string[][](二维字符串数组),每组元素是一组异位词,组内顺序、组的顺序均无要求;

  • 输入边界:数组可能为空(需兼容空输入)、字符串可能为空串(如 "" 单独为一组);

  • 字符范围:默认仅包含小写英文字母(无需处理大写、符号、数字,为解法优化提供前提)。

解题核心思路:找到异位词的"唯一标识",将所有具有相同标识的字符串归为一组------这是哈希表的典型应用场景,通过"键(标识)-值(异位词组)"的映射,实现高效分组。

二、给定解法完整解析(经典哈希+排序,最易理解)

先贴出可直接运行的正确解法(已修正潜在问题),再逐行拆解逻辑、分析时空复杂度,让你清晰每一步的意义。

typescript 复制代码
function groupAnagrams(strs: string[]): string[][] {
  // 1. 定义哈希表(对象形式),key是异位词的唯一标识,value是该组异位词数组
  const map: { [key: string]: string[] } = {};
  
  // 2. 遍历字符串数组,逐个处理每个字符串
  for (const str of strs) {
    // 3. 核心:生成当前字符串的唯一标识key
    // 拆分字符串→排序→拼接,异位词排序后会得到完全相同的字符串
    const key = str.split('').sort().join('');
    
    // 4. 判断key是否已存在,决定是新增分组还是加入已有分组
    if (map[key]) {
      // 若key存在,将当前字符串推入对应数组
      map[key].push(str);
    } else {
      // 若key不存在,初始化该分组(数组中先存入当前字符串)
      map[key] = [str];
    }
  }
  
  // 5. 提取哈希表的所有value,转为二维数组返回(满足题目返回值要求)
  return Object.values(map);
}

2.1 核心逻辑拆解(三步搞定分组)

这道题的解法精髓的是"找唯一标识+哈希映射",整个流程可简化为3步,逻辑清晰且易记忆:

  1. 初始化哈希表:用对象 map 存储分组结果,key 用于区分不同的异位词组,value 是该组所有字符串的集合(数组形式);

  2. 生成唯一标识(最关键一步):对于每个字符串 str,通过 split('') 拆分为字符数组,sort() 对字符数组排序,再 join('') 拼接回字符串------所有异位词经过这一步,都会得到完全相同的 key(例:"eat" 和 "tea" 排序后都是 "aet");

  3. 分组+返回结果:遍历过程中,将当前字符串归入对应 key 的数组;遍历结束后,用 Object.values(map) 提取所有分组数组,转为二维数组返回,完美匹配题目要求。

2.2 时空复杂度分析

刷题时,除了写出正确解法,更要理解时空开销,这是面试中区分基础和进阶的关键:

  • 时间复杂度:O(n * k log k)

  • n:字符串数组 strs 的长度(遍历数组的时间开销);

  • k:数组中最长字符串的长度(单个字符串拆分、排序、拼接的时间开销,核心是排序的 O(k log k));

  • 总开销:遍历每个字符串,每个字符串做一次排序,因此总时间复杂度是两者的乘积。

  • 空间复杂度:O(n * k)

  • 哈希表 map 需存储所有字符串(每个字符串都会存入对应分组数组);

  • 最坏情况:所有字符串都不是异位词(每个字符串单独一组),此时 map 存储的内容与原数组一致,空间开销为 O(n * k),属于合理开销。

三、高频易错点规避(重中之重)

这道题看似简单,但很多人在写代码时会踩坑,尤其是 TypeScript 环境下,结合给定解法,重点强调2个易错点:

易错点1:判断 key 是否存在的严谨性

给定解法中 if (map[key]) 虽然能满足大部分场景,但存在隐式类型转换的风险------若某个分组的数组为空(本题场景不会出现,但代码严谨性不足),map[key] 会被转为 false,导致逻辑错误。

优化写法(更严谨):用 Object.prototype.hasOwnProperty.call(map, key) 判断 key 是否是 map 自身的属性,避免原型链上的属性干扰,尤其适合复杂场景:

typescript 复制代码
if (Object.prototype.hasOwnProperty.call(map, key)) {
  map[key].push(str);
} else {
  map[key] = [str];
}

易错点2:空输入/空字符串兼容

若输入 strs 为空数组([]),Object.values(map) 会返回空数组,完全符合题目要求;若输入包含空字符串(如 ["", "a"]),空字符串拆分、排序、拼接后还是空字符串(key 为 ""),会单独分为一组,无需额外处理,给定解法已完美兼容。

四、解法优化(空间换时间,避免排序开销)

给定解法的核心是"排序+哈希",优点是简洁易理解,但排序的 O(k log k) 开销可以优化------借鉴 LeetCode 242 题的"计数思想",用 26 个字母的计数作为唯一 key,避免排序。

优化解法(计数+哈希,时间效率更高)

核心思路:用长度为26的数组,统计每个字符串中 a-z 的出现次数,将计数数组转为字符串作为 key(如 "eat" 对应 "1,0,0,0,1,0,...0,1"),异位词的计数数组完全相同,因此 key 也相同。

typescript 复制代码
function groupAnagrams(strs: string[]): string[][] {
  const map: { [key: string]: string[] } = {};
  for (const str of strs) {
    // 初始化26个字母的计数数组,默认值为0(a-z对应索引0-25)
    const count = new Array(26).fill(0);
    for (const char of str) {
      // 计算当前字符对应的索引,累加计数
      const index = char.charCodeAt(0) - 'a'.charCodeAt(0);
      count[index]++;
    }
    // 计数数组转为字符串作为key(用逗号分隔,避免数字拼接歧义)
    const key = count.join(',');
    // 分组逻辑与原解法一致
    if (Object.prototype.hasOwnProperty.call(map, key)) {
      map[key].push(str);
    } else {
      map[key] = [str];
    }
  }
  return Object.values(map);
}

优化效果对比

解法类型 时间复杂度 空间复杂度 核心优势
排序+哈希(原解法) O(n * k log k) O(n * k) 简洁易理解,代码量少,适合面试基础写法
计数+哈希(优化解法) O(n * k) O(n * k) 时间效率更高,避免排序开销,适合字符串较长场景

五、刷题小贴士(举一反三)

49题作为242题的进阶题,核心思想都是"异位词的唯一标识",掌握这道题后,可轻松迁移到同类题目:

  • 举一反三:242题(判断异位词)可看作是49题的简化版------只需判断两个字符串的"唯一标识"(排序后/计数后)是否相同即可;

  • 面试建议:优先掌握"排序+哈希"解法(易写易说),若面试官追问优化方向,再说出"计数+哈希"解法,体现对时空复杂度的深度理解;

  • 拓展思考:若题目中包含大写字母、数字或符号,只需调整"唯一标识"的生成逻辑(如区分大小写、增加计数数组长度),核心思路不变。

六、总结

LeetCode 49. 字母异位词分组,是一道"基础但不简单"的中等题------核心考察哈希表的应用,同时兼顾代码严谨性和时空复杂度优化。

给定的"排序+哈希"解法,是最适合刷题和面试的基础写法,逻辑清晰、代码简洁,只需避开"返回值错误"等常见坑点,就能轻松通过所有测试用例;而"计数+哈希"解法则是进阶优化,通过避免排序开销,进一步提升时间效率,体现刷题的深度。

相关推荐
梵刹古音2 小时前
【C语言】 数组函数与排序算法
c语言·算法·排序算法
CHU7290352 小时前
废品回收小程序前端功能设计逻辑与实践
前端·小程序
lzhdim2 小时前
微星首款全白设计的M-ATX小板! MPG B850M EDGE TIMAX WIF刀锋 钛评测:性能媲美顶级X870E主板
前端·edge
恋猫de小郭2 小时前
小米 HyperOS 4 大变样?核心应用以 Rust / Flutter 重写,不兼容老系统
android·前端·人工智能·flutter·ios
李火火的安全圈2 小时前
基于Yakit、Wavely实现CVE-2025-55182(React Server Components(RSC)) 反序列化漏洞挖掘和POC编写
前端·react.js
胖咕噜的稞达鸭2 小时前
算法日记:穷举vs暴搜vs深搜vs回溯vs剪枝--全排列
算法·深度优先·剪枝
Figo_Cheung2 小时前
Figo关于热、声、光的物理本质辨析——从根本上解释了光速的恒定性与声速的介质依赖性,揭示了光热转换的微观场论机制
算法·机器学习
格林威2 小时前
Baumer相机轴承滚珠缺失检测:用于精密装配验证的 6 个核心算法,附 OpenCV+Halcon 实战代码!
人工智能·opencv·算法·计算机视觉·视觉检测·工业相机·堡盟相机
一起养小猫2 小时前
Flutter for OpenHarmony 实战:2048游戏算法与优化深度解析
算法·flutter·游戏