「字母异位词」是字符串类题目中极具代表性的考点------从简单的 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步,逻辑清晰且易记忆:
-
初始化哈希表:用对象 map 存储分组结果,key 用于区分不同的异位词组,value 是该组所有字符串的集合(数组形式);
-
生成唯一标识(最关键一步):对于每个字符串 str,通过
split('')拆分为字符数组,sort()对字符数组排序,再join('')拼接回字符串------所有异位词经过这一步,都会得到完全相同的 key(例:"eat" 和 "tea" 排序后都是 "aet"); -
分组+返回结果:遍历过程中,将当前字符串归入对应 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. 字母异位词分组,是一道"基础但不简单"的中等题------核心考察哈希表的应用,同时兼顾代码严谨性和时空复杂度优化。
给定的"排序+哈希"解法,是最适合刷题和面试的基础写法,逻辑清晰、代码简洁,只需避开"返回值错误"等常见坑点,就能轻松通过所有测试用例;而"计数+哈希"解法则是进阶优化,通过避免排序开销,进一步提升时间效率,体现刷题的深度。