不知道你们刷哈希表专题有没有这种错觉 ------一看题目要 "分组",脑子里立刻蹦出 "用哈希表啊",觉得这题稳了。结果真上手写,才发现 "用什么当 key" 才是这道题真正的考点。
这道题我第一次写的时候光速写完排序版,还嫌题太简单,后来深入琢磨才发现里面藏着不少小细节。
题目说人话
给你一堆小写字母组成的字符串,把 "字母完全相同、只是顺序不一样" 的单词归到同一组,最后返回所有分组就行。比如 "eat"、"tea"、"ate" 是一伙的,"tan" 和 "nat" 是另一伙的。
思路一:排序当 key,闭眼写都不会错
这应该是 90% 的人第一眼能想到的解法。道理特别朴素:两个单词如果是异位词,把字母按顺序排完之后,肯定长得一模一样。
那我把每个单词排好序的结果当哈希表的 key,原单词往对应的 value 数组里丢,最后把所有 value 拎出来不就完事了?
思路特别顺,写起来也毫无门槛。
javascript
var groupAnagrams = function(strs) {
const map = new Map();
for (const str of strs) {
// js字符串没有原生sort,只能拆成数组排序再拼回去
const key = str.split('').sort().join('');
// 没这个key就先初始化一个空数组
if (!map.has(key)) {
map.set(key, []);
}
// 把原单词塞进对应分组里
map.get(key).push(str);
}
// 把所有分组取出来就是答案
return Array.from(map.values());
};
这个写法面试的时候优先写,速度快、不容易错,保底绝对没问题。但要是遇到字符串特别长的场景,排序的开销就上来了,这时候就有更优的写法。
思路二:计数当 key,面试官追问优化就说它
写完排序版我当时觉得这题就这,直到后来突然反应过来 ------题目里说了只有小写字母,一共就 26 个,我数每个字母出现多少次不就行了?
异位词的字母计数肯定完全相同,这个计数结果也能当唯一 key 啊。不用排序,遍历一遍单词就能统计完,理论上效率更高。
具体就是弄个长度 26 的数组,遍历单词的时候给对应字母计数,最后把这个数组转成字符串当 key。
这里说个我踩过的巨蠢的坑 ------一开始我直接把数组拼成纯数字字符串,比如 1,0,1... 拼成 "101...",结果遇到字母出现次数超过 9 的情况直接翻车。比如 a 出现 1 次、b 出现 12 次,和 a 出现 11 次、b 出现 2 次,拼出来的字符串可能一样,直接哈希冲突了。后来加了个 # 当分隔符,每个数字之间隔开,才彻底解决这个问题。
javascript
var groupAnagrams = function(strs) {
const map = new Map();
for (const str of strs) {
// 初始化26个字母的计数,初始都是0
const count = new Array(26).fill(0);
for (const c of str) {
// 用ASCII码算下标,a对应0,b对应1...
count[c.charCodeAt() - 'a'.charCodeAt()]++;
}
// 用#拼接避免数字位数不同导致冲突,别学我一开始忘了加
const key = count.join('#');
if (!map.has(key)) {
map.set(key, []);
}
map.get(key).push(str);
}
return Array.from(map.values());
};
偷懒精简版:用普通对象更短
写完 Map 版本我还琢磨出个更偷懒的写法,把 Map 换成普通对象,代码能再短一截。
javascript
var groupAnagrams = function(strs) {
const map = {};
for (let s of strs) {
const count = new Array(26).fill(0);
for (let c of s) {
count[c.charCodeAt() - 'a'.charCodeAt()]++;
}
// 对象会自动把数组转成逗号分隔的字符串,天然带分隔符不冲突
map[count] ? map[count].push(s) : map[count] = [s];
}
return Object.values(map);
};
原理说穿了很简单:JS 普通对象的键只能是字符串,你往里丢数组,它会自动调用 toString() 转成逗号拼接的字符串。相当于自带了分隔符,完全不会出现数字位数冲突的问题。
这个写法刷 LeetCode 完全能过,写起来飞快。但有个坑要记牢:换成 Map 绝对不能这么写。Map 支持对象键且按引用匹配,每次循环 new 的数组都是不同引用,会导致每个单词都自成一组,直接全错。面试的时候更推荐显式写 join,意图更清晰,也不容易踩数据结构的坑。
简单说下效率
假设 n 是字符串个数,每个字符串平均长度是 k。
- 排序法:每个单词排序要 O (klogk),总时间复杂度 O (n*klogk)
- 计数法:不用排序,每个单词遍历一遍,总时间复杂度 O (n*k)
空间上两者差不多,都是存所有字符串,O (nk)。日常刷题里除非单词特别长,不然两者体感差不太多,排序法胜在写得快、不容易错。
最后唠两句
其实这道题本质就是哈希表分组的经典套路 ------找同一类元素的唯一标识。不管是排序还是计数,都是在给异位词找一个 "身份证",有了身份证,往哈希表里一丢就自动分好组了。
给你们避几个我踩过的坑:
- 别想着用字符和、字符乘积当 key,冲突概率高到离谱,纯给自己找事
- 计数法一定要加分隔符,不然长字符串必错
- 空字符串不用额外处理,上面两种写法都天然兼容
你们刷这道题的时候第一反应是哪种写法?有没有踩过什么更有意思的坑?评论区聊聊,我都会回的~觉得有用的话点个赞,让更多刷题的小伙伴能看到呀。