【LeetCode 刷题日志】哈希表经典双题:242. 有效的字母异位词 & 49. 字母异位词分组(C++ 详解)
本文是我的 LeetCode 刷题日志系列之一,用 C++ 语言深入讲解两道经典的哈希表题目 ——
242. 有效的字母异位词
和
49. 字母异位词分组
。这两题一脉相承,是面试高频题,也是掌握哈希表思想的绝佳入门组合。文章包含完整的题目描述、多种解题思路、C++ 代码实现、复杂度分析、踩坑记录和学习感悟,
适合算法新手和有一定基础想要巩固哈希表的同学
阅读。
📑 本文目录
- 一、力扣 242. 有效的字母异位词
- 二、力扣 49. 字母异位词分组
- 三、两题关联与进阶思考
- 四、刷题心得与学习感悟
- 五、总结
📋 本篇日志包含的题目
| 1 | 242 | 有效的字母异位词 | 简单 | 哈希表 / 数组计数 |
| 2 | 49 | 字母异位词分组 | 中等 | 哈希表 / 排序 / 字符串编码 |
💡
为什么把这两题放在一起?
第 242 题是判断两个字符串是否为字母异位词,第 49 题是将一组字符串中的字母异位词分组。后者是前者的进阶和推广,掌握了 242 的核心思想,49 就水到渠成。
一、力扣 242. 有效的字母异位词
1.1 题目描述
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意: 若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例 1:
输入: s = "anagram", t = "nagaram"
输出: true
示例 2:
输入: s = "rat", t = "car"
输出: false
提示:
-
1 <= s.length, t.length <= 5 * 10^4
-
s 和 t 仅包含小写字母
进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
1.2 解题思路
思路一:排序法(最直观)
核心思想: 如果两个字符串是字母异位词,那么把它们排序后得到的字符串一定完全相同。
步骤:
先判断两个字符串长度是否相等,不相等直接返回 false
分别对两个字符串进行排序
比较排序后的字符串是否相等
优点: 思路简单,代码短,容易想到缺点: 排序的时间复杂度较高,为 O(n log n)n)
思路二:哈希表计数法(最优解)
核心思想: 题目明确说明只包含小写字母,共 26 个,因此可以用一个长度为 26 的数组来模拟哈希表,统计每个字符出现的次数。
步骤:
先判断两个字符串长度是否相等,不相等直接返回 false
创建一个长度为 26 的整型数组 count,初始化为 0
遍历字符串 s,对每个字符,在 count 数组对应位置 +1
遍历字符串 t,对每个字符,在 count 数组对应位置 -1
最后检查 count 数组是否全部为 0,若是则返回 true,否则返回 false
优化技巧: 在遍历 t 的过程中,如果某个位置减到负数,可以提前返回 false,减少不必要的遍历。
1.3 C++ 代码实现
方法一:排序法
class Solution {
public:
bool isAnagram(string s, string t) {
// 长度不同直接返回false
if (s.length() != t.length()) {
return false;
}
// 排序后比较
sort(s.begin(), s.end());
sort(t.begin(), t.end());
return s == t;
}
};
方法二:数组计数法(推荐)
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.length() != t.length()) {
return false;
}
vector<int> table(26, 0);
for (auto& ch: s) {
table[ch – 'a']++;
}
for (auto& ch: t) {
table[ch – 'a']—;
if (table[ch – 'a'] < 0) {
return false;
}
}
return true;
}
};
1.4 复杂度分析
| 排序法 | O(n log n) | O (1) 或 O (log n)(取决于排序算法的栈空间) |
| 哈希表法 | O(n) | O (1)(字符集大小固定) |
其中 n 为字符串长度。
1.5 踩坑记录与注意点
忘记先判断长度:如果两个字符串长度不同,一定不是字母异位词,可以提前返回,这是最基本的剪枝。
数组初始化问题:int count[26] = {0}; 可以将数组全部初始化为 0,但如果写成 int count[26]; 则数组元素是未定义的随机值,会导致错误。
字符偏移计算:c – 'a' 可以将小写字母映射到 0-25 的下标,这是常用技巧。
提前剪枝的位置:在遍历 t 时减到负数就返回 false,这个优化只在长度相等的前提下有效(我们已经先判断了长度),因为如果长度相等,只要有一个位置是负数,最终一定不可能全为 0。
二、力扣 49. 字母异位词分组
2.1 题目描述
给你一个字符串数组,请你将字母异位词组合在一起。可以按任意顺序返回结果列表。
字母异位词指字母相同,但排列不同的字符串。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
提示:
-
1 <= strs.length <= 10^4
-
0 <= strs[i].length <= 100
-
strs[i] 仅包含小写字母
2.2 解题思路
思路一:排序 + 哈希表(最常用)
核心思想: 互为字母异位词的字符串,排序后的结果相同。因此可以将排序后的字符串作为哈希表的 key,原始字符串作为 value 存入对应的列表中。
步骤:
创建一个 unordered_map<string, vector<string>> 哈希表
遍历每个字符串:
-
对字符串进行排序,得到 key
-
将原始字符串添加到哈希表中 key 对应的 vector 中
思路二:字符计数编码 + 哈希表(优化版)核心思想: 排序的时间复杂度是 O(k log k)(k 为字符串长度),我们可以用计数的方式生成一个唯一的编码作为 key,时间复杂度降为 O(k)。)。
编码方式: 由于只有 26 个小写字母,我们可以统计每个字符出现的次数,然后用一种分隔符(如 #)拼接成一个字符串作为 key。例如 "aab" 的计数是 a:2, b:1,编码为 "2#1#0#0#…#0"(共 26 个数字,用 # 分隔)。)。
步骤:
创建哈希表,key 为编码字符串,value 为字符串列表
遍历每个字符串:
-
统计 26 个字母各自出现的次数
-
将计数数组编码成唯一字符串作为 key
-
将原始字符串加入对应列表
2.3 C++ 代码实现
方法一:排序 + 哈希表
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp;
for (string& str: strs) {
// 排序后的字符串作为key
string key = str;
sort(key.begin(), key.end());
mp[key].emplace_back(str);
}
// 收集结果
vector<vector<string>> ans;
for (auto it = mp.begin(); it != mp.end(); ++it) {
ans.emplace_back(it->second);
}
return ans;
}
};
方法二:利用 C++17 结构化绑定简化代码
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp;
for (auto& str : strs) {
string key = str;
sort(key.begin(), key.end());
mp[key].push_back(str);
}
vector<vector<string>> result;
// C++17结构化绑定
for (auto& [key, group] : mp) {
result.push_back(move(group)); // 使用move避免拷贝
}
return result;
}
};
2.4 复杂度分析
设 n 为字符串数组的长度,k 为每个字符串的最大长度。
| 排序 + 哈希表 | O(n × k log k) | O(n × k) |
说明:
-
排序法中,每个字符串排序需要 O (k log k),共 n 个字符串
-
空间上,哈希表需要存储所有字符串,为 O (n × k)
2.5 踩坑记录与注意点
编码必须用分隔符:如果直接把计数拼接成数字字符串,会产生歧义。例如 count = [1, 23] 和 count = [12, 3] 拼接后都是 "123",必须用 # 等分隔符区分。
空字符串的处理:题目中可能出现空字符串 "",排序后还是 "",计数全为 0,编码也能正确处理,不需要特殊判断。
unordered_map vs map:这里用 unordered_map 平均时间复杂度更低(O (1) 插入),map 是 O (log n)。题目不要求结果有序,所以用 unordered_map。
push_back vs emplace_back:对于字符串这种可以移动的对象,emplace_back 有时更高效,但 push_back 配合右值引用也能达到同样效果。
结果顺序:题目说 “可以按任意顺序返回结果列表”,所以不需要对最终结果排序。
三、两题关联与进阶思考
3.1 从 242 到 49 的思维跃迁
| 输入 | 两个字符串 | 一组字符串 |
| 问题 | 判断是否为异位词 | 将异位词归类 |
| 核心 | 计数比较 | 哈希分组 |
| key | 不需要 key,直接比计数数组 | 排序字符串 / 计数编码 |
思维提炼: 242 题中我们用计数数组判断两个字符串是否匹配;49 题中,我们需要把 “匹配” 这个关系推广到一组字符串中,自然就想到用哈希表把相同特征的字符串归到一起。这就是从 “两两比较” 到 “按特征分组” 的思维升级。
3.2 可复用模板汇总(其他题目直接套用)
这两道题虽然表面不同,但提炼出的模板可以复用到大量同类题目中。以下是从本题中总结的 4 个固定模板,每个模板给出代码、适用场景和可直接套用的题目。
模板一:字符计数数组模板(源自 242 题)
核心思想: 当字符集范围有限且较小时(如小写字母 26 个、ASCII 128 个),用数组代替 unordered_map 进行计数,效率更高。
// 模板:字符频率计数
// 适用:仅含小写字母 / 大写字母 / ASCII字符 的字符串统计问题
vector<int> countChars(const string& s) {
vector<int> count(26, 0); // 26个小写字母;大写字母用26,ASCII用128
for (char c : s) {
count[c – 'a']++; // 小写字母偏移;大写字母用 c – 'A'
}
return count;
}
// 模板:两个字符串计数比较(判断是否互为异位词/变位词)
bool compareCount(const string& s, const string& t) {
if (s.length() != t.length()) return false; // 长度剪枝
int count[26] = {0};
for (char c : s) count[c – 'a']++;
for (char c : t) {
count[c – 'a']—;
if (count[c – 'a'] < 0) return false; // 提前剪枝
}
return true;
}
适用场景:
- 字符频率统计与比较
- 判断两个字符串是否为异位词 / 变位词
- 找字符串中第一个不重复的字符
- 赎金信(字符串能否由另一个字符串构造)
可直接套用的题目:
| 242 | 有效的字母异位词 | 直接使用 compareCount |
| 383 | 赎金信 | 计数后比较,magazine 中每个字符数 >= ransomNote |
| 387 | 字符串中的第一个唯一字符 | 计数后找第一个 count==1 的字符 |
| 409 | 最长回文串 | 统计字符频率,偶数全用,奇数取偶数部分+1 |
| 438 | 找到字符串中所有字母异位词 | 滑动窗口 + 计数数组比较 |
模板二:哈希表特征分组模板(源自 49 题)
核心思想: 对于一组元素,提取每个元素的"特征"作为哈希表的 key,将相同特征的元素归为一组。这是分组类问题的通用解法。
// 模板:按特征分组
// 适用:需要将一组元素按某种共同特征归类的问题
template<typename T, typename KeyType>
vector<vector<T>> groupByFeature(const vector<T>& items,
function<KeyType(const T&)> extractFeature) {
unordered_map<KeyType, vector<T>> mp;
for (const T& item : items) {
KeyType key = extractFeature(item); // 提取特征作为key
mp[key].push_back(item);
}
vector<vector<T>> result;
for (auto& [key, group] : mp) {
result.push_back(move(group));
}
return result;
}
适用场景:
- 将一组元素按共同特征分组
- 查找重复元素(分组后组大小 > 1)
- 统计每组的数量 / 最大值 / 最小值
可直接套用的题目:
| 49 | 字母异位词分组 | 排序后的字符串 / 计数编码 |
| 249 | 移位字符串分组 | 每个字符与首字符的差值序列 |
| 609 | 在系统中查找重复文件 | 文件内容(MD5 / 实际内容) |
| 811 | 子域名访问计数 | 各级域名作为 key,计数累加 |
| 2260 | 必须拿起的最小连续卡牌数 | 按卡牌值分组,找同组最小间距 |
模板三:排序作 Key 模板(源自 49 题方法一)
核心思想: 当元素的"内容"相同但"顺序"不同时,排序后得到的结果相同,可以作为哈希表的 key。这是最简洁的特征提取方式。
// 模板:排序后作为key进行分组
// 适用:顺序无关、内容相同的元素分组
vector<vector<string>> groupBySort(const vector<string>& strs) {
unordered_map<string, vector<string>> mp;
for (const string& s : strs) {
string key = s;
sort(key.begin(), key.end()); // 排序后作为key
mp[key].push_back(s);
}
vector<vector<string>> result;
for (auto& [key, group] : mp) {
result.push_back(move(group));
}
return result;
}
适用场景:
- 字母异位词分组
- 变位词 / 相同字母组合的归类
- 数组中元素相同但排列不同的分组
优缺点:
- 代码极简,不容易出错
- 适用于任何可排序的元素类型
- 时间复杂度含排序因子 O(k log k),k 为元素长度
- 要求元素可排序(字符串、数组等可以,自定义结构需定义比较函数)
可直接套用的题目: 49 题、249 题(需转换后排序)、面试题 10.02(变位词组)
模板选择速查表
| 两个字符串比较是否为异位词 | 模板一:计数数组比较 | O(n) | O(1) |
| 字符集不限 / 代码求简洁 + 分组 | 模板三:排序作 key | O(n x k log k) | O(n x k) |
| 通用分组(特征自定义) | 模板二:哈希表特征分组 | 取决于特征提取 | O(n x k) |
实战建议: 面试中如果字符串长度 k <= 100,排序法(模板三)代码短、不易出错,是首选。两个字符串比较的场景永远用模板一。
四、刷题心得与学习感悟
4.1 这两题能学到什么?
哈希表的核心思想:用空间换时间,通过哈希函数将元素映射到唯一标识,实现 O (1) 的查找和插入。
数组模拟哈希表的技巧:当 key 的范围有限且较小时(如 26 个小写字母),用数组代替 unordered_map 更高效。
特征提取的思维:对于分组类问题,关键是找到一个能唯一标识组内元素的 “特征”,并将其作为哈希表的 key。
时间复杂度的权衡:排序法代码简洁但时间稍高,计数法时间更优但代码稍长,要根据数据规模和实际场景选择。
4.2 新手常见误区
1暴力解法思维:新手做 49 题时容易想到两两比较(O(n² × k)),这在 n=10^4 时会超时。要建立 “用哈希表分组” 的意识。识。
忽略边界条件:空字符串、长度为 1 的字符串、全相同的字符串等边界情况要考虑到。
编码歧义问题:计数编码时忘记加分隔符,导致不同计数产生相同 key,这是隐蔽的 bug。
不做长度剪枝:242 题中忘记先比较长度,虽然结果不错,但少了一个重要的优化点。
4.3 我的刷题方法分享
先暴力再优化:先想清楚最朴素的解法(即使会超时),再逐步优化,这样能更好地理解优化的本质。
一题多解:每道题至少想两种解法,比较优劣,拓宽思路。比如这两题都有排序法和计数法。
归类总结:做完题后想想这道题属于什么类型,和之前做过的哪些题思路相似,建立知识网络。
写刷题日志:把解题思路、踩坑点、优化过程记录下来,不仅加深印象,也方便日后复习。
五、总结
本文详细讲解了 LeetCode 242(有效的字母异位词)和 49(字母异位词分组)两道经典题目:
242 题:核心是用数组计数比较两个字符串的字符频率,时间 O(n),空间 O(1),是哈希表思想的基础应用。用。
-
49 题:核心是提取每个字符串的特征(排序结果或计数编码)作为哈希表的 key 进行分组,时间 O (n×k) 或 O (n×k log k),是哈希分组思想的典型应用。
-
两题关系:49 题是 242 题的推广,从 “判断两个” 升级为 “分组多个”,体现了哈希表从比较到分组的思维跃迁。
这两道题是面试高频题,也是掌握哈希表的必刷题。希望这篇日志能帮助你理解其中的思想,也欢迎在评论区交流你的解法和心得!
下一篇预告
:继续哈希表专题,将讲解【283.移动零】和【11.盛最多水的容器】,敬请期待! 如果本文对你有帮助,欢迎点赞 👍、收藏 ⭐、关注我,一起在算法之路上进步!
网硕互联帮助中心




评论前必须登录!
注册