LeetCode 49. 字母异位词分组(哈希表 + 排序)
一、完整代码(精简注释)
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
// key:排序后的字符串,value:对应的字母异位词集合
unordered_map<string, vector<string>> res;
for (auto x : strs) {
string key = x;
sort(key.begin(), key.end()); // 排序后作为哈希表的 key
res[key].emplace_back(x); // 将原字符串加入对应的 vector
}
vector<vector<string>> ans;
// 遍历哈希表,将每组字母异位词存入结果
for (auto [x, y] : res) {
ans.emplace_back(y);
}
return ans;
}
};
二、解题思路
本题要求将字符串数组中的字母异位词分到同一组。
字母异位词指的是:两个字符串包含相同的字符,且每个字符出现的次数相同,只是排列顺序不同。
例如:
"eat"、"tea"、"ate"
这三个字符串排序后都是:
"aet"
因此,可以利用排序后的字符串作为哈希表的 key,将所有具有相同 key 的字符串存储在同一个 vector<string> 中。
核心思想:字母异位词经过排序后,一定得到相同的字符串。
三、代码关键步骤
1. 创建哈希表
unordered_map<string, vector<string>> res;
这里需要重点理解哈希表的结构。
-
key:排序后的字符串。 -
value:一个vector<string>,存储所有对应的原始字符串。
例如:
res["aet"] = {"eat", "tea", "ate"};
res["ant"] = {"tan", "nat"};
res["abt"] = {"bat"};
注意:哈希表中的 key 是唯一的,但每个 key 对应的 vector 可以存储多个字符串。
2. 遍历字符串数组
for (auto x : strs) {
string key = x;
sort(key.begin(), key.end());
}
这里:
string key = x;
相当于复制当前字符串。
例如:
x = "eat";
key = "eat";
排序后:
x = "eat"; // 原字符串不变
key = "aet"; // 排序后的字符串
为什么不直接排序 x?
因为题目要求返回原始字符串,而不是排序后的字符串。
3. 将字符串存入对应的 vector
res[key].emplace_back(x);
这是本题最关键的一行代码。
可以分成两部分理解:
res[key]
表示获取哈希表中 key 对应的 vector<string>。
如果 key 不存在,operator[] 会自动创建一个空的 vector<string>。
然后:
.emplace_back(x)
将原始字符串 x 添加到该 vector 的末尾。
例如:
res["aet"].emplace_back("eat");
res["aet"].emplace_back("tea");
res["aet"].emplace_back("ate");
最终得到:
res["aet"] = {"eat", "tea", "ate"};
因为 key 相同,所以三个字符串都被添加到同一个 vector 中。
4. 将哈希表中的 value 存入答案
vector<vector<string>> ans;
for (auto [x, y] : res) {
ans.emplace_back(y);
}
这里的:
auto [x, y] : res
是 C++17 的结构化绑定。
其中:
-
x:哈希表的 key,即排序后的字符串。 -
y:哈希表的 value,即对应的字符串数组。
例如:
x = "aet";
y = {"eat", "tea", "ate"};
执行:
ans.emplace_back(y);
相当于将整个 vector<string> 添加到二维数组 ans 中。
最终得到:
ans = {
{"eat", "tea", "ate"},
{"tan", "nat"},
{"bat"}
};
由于使用的是 unordered_map,所以最终各组之间的顺序不固定。
四、举例模拟
假设输入:
strs = {"eat", "tea", "tan", "ate", "nat", "bat"};
遍历过程如下:
| 当前字符串 x | 排序后的 key | 哈希表操作 |
|---|---|---|
"eat" |
"aet" |
创建分组,加入 "eat" |
"tea" |
"aet" |
加入已有分组 |
"tan" |
"ant" |
创建新分组 |
"ate" |
"aet" |
加入已有分组 |
"nat" |
"ant" |
加入已有分组 |
"bat" |
"abt" |
创建新分组 |
遍历结束后,哈希表的逻辑结构为:
key:"aet"
"eat", "tea", "ate"
key:"ant"
"tan", "nat"
key:"abt"
"bat"
最后遍历哈希表,只取出每个 key 对应的 value,组成二维数组。
结果为:
[
["eat", "tea", "ate"],
["tan", "nat"],
["bat"]
]
五、复杂度分析
假设:
-
n表示字符串数组中字符串的数量。 -
k表示字符串的最大长度。
- 时间复杂度:O(n × k log k)
外层循环遍历 n 个字符串:
for (auto x : strs)
每个字符串都需要排序:
sort(key.begin(), key.end());
排序时间复杂度为 O(k log k)。
哈希表查找和插入的平均开销与字符串长度有关,通常为 O(k)。
因此,总的平均时间复杂度为:
\ O(nk\\log k) \\
- 空间复杂度:O(nk)
哈希表需要保存排序后的 key,以及每个分组中的原始字符串。
因此,总的额外空间复杂度为:
\ O(nk) \\
六、最后总结
本题是 哈希表 + 排序 的经典应用,重点掌握以下几点:
-
为什么使用
unordered_map? 因为需要建立排序后字符串与原始字符串集合之间的映射关系,并且不需要对 key 排序。 -
为什么将字符串排序后作为 key? 因为字母异位词排序后一定相同,可以用排序结果作为分组依据。
-
为什么 value 是
vector<string>? 因为同一个 key 可能对应多个原始字符串,需要用 vector 保存一整组。 -
为什么
res[key]可以直接使用? 因为unordered_map的operator[]在 key 不存在时,会自动创建一个默认初始化的 value。这里就是空的vector<string>。 -
为什么使用
emplace_back(x)? 因为需要将当前原始字符串添加到对应分组的末尾。 -
为什么最后还需要
ans? 因为题目要求返回vector<vector<string>>,而不是哈希表,所以需要提取所有 value。 -
为什么结果的分组顺序不固定? 因为
unordered_map是无序容器,不保证遍历顺序。
七、与两数之和的区别
你刚才做的两数之和与这道题都使用了 unordered_map,但用途有所不同。
| 对比 | 两数之和 | 字母异位词分组 |
|---|---|---|
| 哈希表类型 | unordered_map<int,int> |
unordered_map<string,vector<string>> |
| key | 元素值 | 排序后的字符串 |
| value | 元素下标 | 原始字符串集合 |
| 核心操作 | find() 查找 |
emplace_back() 分组 |
| 主要作用 | 快速寻找目标元素 | 将相同特征的元素归类 |
一句话总结:
两数之和利用哈希表快速查找元素,而字母异位词分组利用哈希表的 key 唯一性,将具有相同特征的字符串存入同一个 vector。
这道题最重要的是理解:
res[key].emplace_back(x);
相同的 key 对应同一个 vector,不同的 key 对应不同的 vector。
这就是哈希表实现分组的核心思想。