LC 49 字母异位词分组:想到哈希表很简单,选对 key 才是精髓

不知道你们刷哈希表专题有没有这种错觉 ------一看题目要 "分组",脑子里立刻蹦出 "用哈希表啊",觉得这题稳了。结果真上手写,才发现 "用什么当 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,冲突概率高到离谱,纯给自己找事
  • 计数法一定要加分隔符,不然长字符串必错
  • 空字符串不用额外处理,上面两种写法都天然兼容

你们刷这道题的时候第一反应是哪种写法?有没有踩过什么更有意思的坑?评论区聊聊,我都会回的~觉得有用的话点个赞,让更多刷题的小伙伴能看到呀。

相关推荐
kyriewen3 小时前
用了半年 Claude Code 后,我尝试关掉它写了一周代码——结果比想象中严重
前端·javascript·ai编程
山河木马5 小时前
矩阵专题0-webGL中的矩阵
javascript·webgl·计算机图形学
Asize6 小时前
多模态生图:从 Vite 工程化到前端调用 Qwen Image
javascript·人工智能·后端
陳陈陳6 小时前
从Token到Embedding:一篇文章搞懂大模型的「文字数学变形记」
前端·javascript·ai编程
用户938515635076 小时前
从 O(n²) 到 O(nlogn):一文读懂快速排序的“快”与“妙”
javascript·算法
橘子星6 小时前
LLM 无状态架构实践:从原理到代码落地
前端·javascript·人工智能
To_OC7 小时前
手写快排次次翻车?别死背快排模板了,这才是面试官想听的底层逻辑
javascript·算法·排序算法
饼干哥哥8 小时前
Reddit VOC调研太慢?搭一个AI专家团队半小时洞察任何品类|以猫用饮水机为例
人工智能·算法·ai编程
风止何安啊8 小时前
网课倍速痛点解决:一套前端代码实现自由控速播放器
前端·javascript·node.js