一看到"字母异位词分组"这道题,我的第一反应是:LeetCode 编号 49,哈希表分类,面试高频,看起来好像就是"哈希 + 字符串分组"的组合。可真正动手做的时候会发现,简单的是思路,容易翻车的是哈希键的定义。如果用不好键,或者没想清楚"什么能作为键、什么不能作为键",代码写出来不是性能差,就是直接编译报错。这篇文章就把这道题从头到尾拆开,从原理讲到代码,附带我实际刷题和面试候选人的时候反复踩过的坑。
这道题的要求很简单:给你一个字符串数组,把字母异位词分到同一组。所谓字母异位词,就是字母种类和数量完全一样,只是排列顺序不同。比如 "eat"、"tea"、"ate",它们都是三个字母 e、a、t 的排列,所以必须归为一组;"tan" 和 "nat" 也互为异位词;而 "bat" 跟前面任何一组都凑不上。题目最后要输出的就是这些分组结果。
1. 读题时先抓住"异位词分组"到底在找什么
1.1 题目的真正难点不是"相同",而是"等价"
很多人上手这道题时,第一反应是去两两比较字符串是否相等。这是自然反应,但它马上会陷入一个泥潭:字符串相等是严格字符按位相同,而字母异位词是"字符集合相同但顺序不同"。你不能简单地用 s1 == s2 判断,得先定义一个"等价关系"。
这个等价关系的定义,是整道题的题眼。
如果两个字符串互为异位词,那么它们必须满足两个条件:字符种类完全一样;每个字符出现的次数完全一样。也就是说,它们拥有的"字符计数向量"相同。比如 "abcc" 和 "cabc",计数都是 a:1、b:1、c:2。这个向量就是它们的公共身份。
所以题目就转化成了:把拥有同一个计数向量的字符串扔进同一个桶里。我们需要的是一种方法,能够快速从任意字符串算出它的"身份标识",并且这个标识必须是稳定、唯一、便于比较的。看到这你应该已经意识到了,哈希表就是干这个的。
1.2 哈希表为什么适合这道题?
哈希表的本质是:通过一个哈希函数,把任意对象映射到一个可比较的键值,然后根据键值把数据放进桶里。这道题需要做的恰恰是:为每个字符串计算一个"归一化后的键",让互为异位词的字符串得到同一个键,然后以键为维度分组。
类比一下就懂了。假设你是快递分拣员,手上有一堆包裹,分拣规则是"只要是收件人姓名相同的包裹就放同一个货架"。这时你不会去读包裹里装的是什么,而是看一眼收件人名字,然后直接丢到对应货架。哈希表就是那个"货架",而收件人名字就是"键"。
在字母异位词分组里,收件人名字不能直接写原始字符串,因为 "eat" 和 "tea" 虽然是同一个人,但字面上不一样。所以你得给每个包裹重新贴一个"规范化标签",让同一组字符串都在标签上写着同一个暗号。
1.3 暴力法,先跑一遍才懂为什么要优化
先看最直观的暴力解法:把数组里每个字符串和其他字符串两两比较,判断是否互为异位词。判断两个字符串是否互为异位词,可以排序后比较,也可以用计数数组比较。
假设有 n 个字符串,每个字符串平均长度为 k,两两比较一轮是 O(n²),每次比较时如果做排序,复杂度是 O(k log k)。总体就是 O(n² k log k)。这个复杂度在 LeetCode 的测试数据下基本是过不去的:字符串数量一多,二次方的开销会迅速膨胀。
暴力法的本质问题是:它把每对字符串都当成潜在的匹配对象,但并没有提取公共身份。而哈希表解法之所以高效,是因为它把"两两比较"降级成了"每人都领一个统一的编号,编号相同的人自动归队"。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分组方案的分水岭:用什么作为哈希键
代码实现的分水岭,不在于你是用 Python 还是 C++,而在于你选择什么作为哈希表的键。整道题的正解,本质上就是选键问题。
2.1 排序键:一行代码完成归一化
最常见的做法是:把字符串里的字符排序,排序后的结果作为键。
"eat" 排序后是 "aet";"tea" 排序后也是 "aet";"ate" 排序后还是 "aet"。三个字符串都映射到同一个键,自然被分到同一组。这背后的道理是:排序不会改变字符种类和数量,只会改变顺序。如果两个字符串互为异位词,它们的字符排序结果必然相同。
这个方案的优点是简单直接,几乎不需要什么特别的数学思考。Python 里写起来尤其爽:
python复制from collections import defaultdict
def group_anagrams(strs):
table = defaultdict(list)
for s in strs:
key = "".join(sorted(s))
table[key].append(s)
return list(table.values())
C++ 的代码也不复杂:
cpp复制#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;
