算法刷题记录 —— 字母异位词分组(Group Anagrams)
题目链接:49. 字母异位词分组
题目分析
这道题刚看到的时候确实没什么头绪。现在回过头来理解,核心思想其实非常简洁:利用哈希表的键值对机制——同一个键只能对应一条记录。
对于"ant"、"tna"、"tan"这样的字母异位词,它们有一个共同的特点:排序之后的字符串完全相同(都是"ant")。既然如此,把排序后的字符串作为哈希表的 key,具有相同字母构成的单词天然就会被归到同一个 key 下面,value 则用一个List<String>来收纳这一组的所有原单词。
本质上这是一种"归一化"思路——给每个单词找一个规范形式,形式相同的归为一组。排序法是最直观的归一化手段,此外也可以用计数法(统计每个字母出现次数)来构建 key,但排序法代码最简洁。
实现
实现一:基础写法
classSolution{publicList<List<String>>groupAnagrams(String[]strs){Map<String,List<String>>map=newHashMap<>();for(Strings:strs){char[]chars=s.toCharArray();Arrays.sort(chars);Stringkey=newString(chars);if(!map.containsKey(key)){map.put(key,newArrayList<>());}map.get(key).add(s);}returnnewArrayList<>(map.values());}}逻辑很直白:遍历每个单词 → 排序得到 key → 如果 key 不存在就新建一个空列表 → 把原单词加进去。最后map.values()直接就是分组结果。
容易踩的两个坑:
char[]不能直接当 key。Java 中数组的equals和hashCode基于对象引用而非内容,两个内容相同的char[]会被当成不同的 key。必须用new String(chars)转成字符串。List的变量作用域。手写时容易把list声明在if块里面,外部拿不到。可以用map.get(key)直接在外部获取引用,更安全也更简洁。
实现二:使用computeIfAbsent简化
classSolution{publicList<List<String>>groupAnagrams(String[]strs){Map<String,List<String>>m=newHashMap<>();for(Strings:strs){char[]chars=s.toCharArray();Arrays.sort(chars);// computeIfAbsent:如果 key 不在哈希表中,则插入一个新的 ArrayListm.computeIfAbsent(newString(chars),_->newArrayList<>()).add(s);}returnnewArrayList<>(m.values());}}思路和实现一一模一样,区别在于用computeIfAbsent一行替代了手动判断containsKey+put+get三步操作。
computeIfAbsent的行为:如果 key 存在,返回对应的 value;如果 key 不存在,执行 Lambda 创建新列表并放入 map,再返回这个新列表。拿到列表引用后直接.add(s)即可。代码量大幅减少,可读性也更好。
关键点总结
① 为什么排序后就能分组?
字母异位词的定义是"字母相同、排列不同"。排序抹平了排列差异——"eat"、"tea"、"ate"排序后全是"aet"。排序结果天然就是分组依据。
②char[]转String不可省略
这是 Java 初学者最容易忽略的细节。数组类型的equals和hashCode是继承自Object的,比较的是内存地址,两个内容完全相同的char[]在 HashMap 眼中是不同的 key。new String(chars)才是值语义的比较。
③computeIfAbsentvs 手动判断
| 方式 | 代码行数 | 可读性 | 适用场景 |
|---|---|---|---|
手动containsKey+put+get | 多 | 适合新手理解流程 | 逻辑较复杂的初始化 |
computeIfAbsent | 一行 | 简洁优雅 | 大多数场景推荐使用 |
小结
字母异位词分组是一道哈希表的经典应用题,难度不高但很能考察对 HashMap 核心特性的理解。抓住"归一化 → 分组"这条主线,无论用排序还是计数都能顺利解决。推荐先用基础写法把流程走通,再尝试用computeIfAbsent优化,两种写法都熟练掌握之后,这类题目基本可以秒过。
