🔥【算法2/100】LeetCode Hot 100 刷题日记:第2题「字母异位词分组」详解 💡
专栏:【算法】LeetCode Hot 100 刷题日记
题目编号:#49
题目名称:字母异位词分组(Group Anagrams)
难度:🟡 中等
标签:哈希表、字符串、排序
📌 题目链接
🧠 题目分析
字母异位词是指由相同字母重新排列形成的不同单词,如"eat"、"tea"和"ate"就是一组字母异位词。本题需要将给定的字符串数组中的字母异位词分组归类。关键在于找到字母异位词的共同特征作为分组的依据。
🔍 小知识补充:
“字母异位词”是英文 “anagram” 的翻译,常出现在密码学、文字游戏和自然语言处理中 🎯。例如:“listen” 和 “silent” 是经典例子。这道题考察的是如何抽象出不变特征来分类数据——这是哈希思想的核心应用之一!
⚙️ 核心算法及代码讲解
本解法采用排序法作为核心算法。其原理是:字母异位词排序后会得到相同的字符串,可以将这个排序后的字符串作为哈希表的键,原始字符串作为值进行分组。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> data; // 哈希表:键为排序后的字符串,值为原字符串列表
for (const auto& s : strs) { // 遍历每个字符串
auto key = s; // 复制原字符串用于排序
sort(key.begin(), key.end()); // 将字符串排序得到键(字母异位词排序后相同)
data[key].push_back(s); // 将原字符串加入对应键的分组中
}
vector<vector<string>> ret; // 定义结果向量
for (const auto& p : data) { // 遍历哈希表中的所有分组
ret.push_back(p.second); // 将每个分组加入结果向量
}
return ret; // 返回最终分组结果
}
};
💡 技巧提示:
- 使用
auto key = s复制字符串是为了避免修改原字符串。 sort(key.begin(), key.end())实现了字符级别的排序,时间复杂度 O(k log k),k 为字符串长度。data[key].push_back(s)自动创建新桶或追加元素,C++ 容器自动扩容。
🧩 解题思路
- 理解问题本质:字母异位词具有相同的字母组成,只是排列顺序不同
- 确定分组特征:将每个字符串排序,字母异位词排序后会得到相同的字符串
- 建立映射关系:使用哈希表,以排序后的字符串为键,原始字符串列表为值
- 遍历处理:对每个字符串进行排序操作,然后将其添加到哈希表对应的分组中
- 输出结果:将哈希表中所有的值收集起来即为最终分组结果
🧠 为什么排序能作为“指纹”?
因为字母异位词的字符集合完全一致,只是顺序不同。排序后它们变成唯一标识符,就像每个人的身份证号一样 👤。这种“规范化”策略是解决分类问题的常用手段。
📊 算法分析
时间复杂度:O(n × k × log k),其中n是字符串数量,k是字符串最大长度。每个字符串排序需要O(k × log k)时间,共n个字符串
空间复杂度:O(n × k),哈希表需要存储所有字符串
优点:实现简单直观,代码易于理解和维护
缺点:当字符串较长时排序操作开销较大
📊 对比其他解法:
| 方法 | 时间复杂度 | 空间复杂度 | 是否可行 |
|---|---|---|---|
| 排序法(本解法) | O(n×k×log k) | O(n×k) | ✅ 简洁高效 |
| 计数法(优化版) | O(n×k) | O(k) | ✅ 更优 |
| 暴力比较 | O(n²×k) | O(1) | ❌ 不推荐 |
✅ 计数法简介:用长度为26的数组记录每个字符出现次数,转为元组作为 key,避免排序。适用于只含小写字母的情况。
💻 代码
// Leetcode49.字母异位词分组
#include <bits/stdc++.h>
#include <unordered_map>
using namespace std;
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> data; // 哈希表:排序后的字符串 -> 原字符串列表
for (const auto& s : strs) { // 遍历每个字符串
auto key = s; // 复制原字符串用于排序
sort(key.begin(), key.end()); // 排序字符串作为键
data[key].push_back(s); // 将原字符串加入对应分组
}
vector<vector<string>> ret; // 定义结果向量
for (const auto& p : data) { // 遍历哈希表
ret.push_back(p.second); // 将分组加入结果
}
return ret; // 返回最终结果
}
};
int main() {
Solution solution;
// 测试用例1
vector<string> strs1 = { "eat", "tea", "tan", "ate", "nat", "bat" };
auto result1 = solution.groupAnagrams(strs1);
cout << "测试用例1结果:" << endl;
for (const auto& group : result1) {
for (const auto& str : group) {
cout << str << " ";
}
cout << endl;
}
// 测试用例2
vector<string> strs2 = { "" };
auto result2 = solution.groupAnagrams(strs2);
cout << "\n测试用例2结果:" << endl;
for (const auto& group : result2) {
for (const auto& str : group) {
cout << "\"" << str << "\" ";
}
cout << endl;
}
// 测试用例3
vector<string> strs3 = { "a" };
auto result3 = solution.groupAnagrams(strs3);
cout << "\n测试用例3结果:" << endl;
for (const auto& group : result3) {
for (const auto& str : group) {
cout << str << " ";
}
cout << endl;
}
return 0;
}
🧪 调试建议:
- 注意空字符串
""的处理,排序后仍为空。 - 单字符字符串
"a"也会被正确分组。 - 可以打印中间变量(如
key)来验证逻辑。
🌱 学习延伸(新增内容)
✅ 举一反三
- 变体1:按字典序输出每组 → 在
ret中排序即可。 - 变体2:返回分组的索引而非字符串 → 可用
vector<int>存储原下标。 - 变体3:支持大小写混合 → 先统一转为小写再处理。
📚 推荐掌握的数据结构
- C++:
unordered_map+vector - Python:
collections.defaultdict(list) - Java:
HashMap<String, List<String>>
💬 面试高频问法
“有没有更高效的解法?”
👉 回答:可以用字符计数法,比如用array[26]统计每个字母频次,然后转成字符串或元组作为 key,避免排序,时间复杂度降至 O(n×k)。
✨ 结语
《字母异位词分组》是一道典型的“分类+哈希”题,展现了如何通过特征提取将复杂问题简化。它不仅是面试常客,更是后续字符串处理题的基础 🧱。掌握它,你就迈出了“哈希思维”的第二步!
📌 下一期预告:LeetCode 热题 100 第3题 —— 最长连续序列(中等)
🎯 题目:给定一个未排序的整型数组,找出数字连续的最长序列的长度。
🔧 核心思路:使用哈希表存储所有数字,遍历每个数字时向左右扩展计算连续序列长度,并标记已访问的数字避免重复计算。
💡 这是哈希表与动态规划结合的经典案例,要求时间复杂度 O(n),挑战更高!