1.

cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> hashmap;
for(int i = 0;i<nums.size();i++){
int complete = target-nums[i];
if(hashmap.find(complete)!=hashmap.end()){
return {hashmap[complete],i};
}
hashmap[nums[i]]=i;
}
return {};
}
};
-
创建一个哈希表,key:数组元素值,value:该元素在数组中的索引- 说明哈希表的用途和结构 -
unordered_map<int,int> hashmap;- 实例化无序哈希表,键和值都是整数类型 -
遍历数组- 标记循环开始的注释 -
for(int i = 0; i < nums.size(); i++)- 从第一个元素遍历到最后一个元素 -
计算当前元素需要的"另一半"是多少- 解释补数计算的目的 -
int complete = target - nums[i];- 计算与当前元素配对能达成目标值的数值在哈希表中查找这个"另一半"是否已经出现过- 说明查找操作的含义 -
if(hashmap.find(complete) != hashmap.end())- 条件判断:如果找到补数(且不是当前元素) -
如果找到了,说明之前遍历过的某个元素 + 当前元素 = target- 解释找到匹配时的逻辑关系 -
返回之前元素的索引和当前元素的索引- 说明返回值的顺序和含义 -
return {hashmap[complete], i};- 返回两个索引:先返回补数的索引,后返回当前元素的索引如果没找到,将当前元素及其索引存入哈希表,供后续元素查找- 解释存储操作的目的和时机 -
hashmap[nums[i]] = i;- 将当前元素值作为键、当前索引作为值存入哈希表 -
理论上题目保证有解,这里返回空数组作为兜底- 说明理论上不会执行到这行,只是编译器要求
2.

cpp
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string,vector<string>>hashmap;
for(int i=0;i<strs.size();i++)
{
string s=strs[i];
sort(s.begin(),s.end());
hashmap[s].push_back(strs[i]);
}
vector<vector<string>> ans;
for(auto it=hashmap.begin();it!=hashmap.end();it++){
ans.emplace_back(it->second);
}
return ans;
}
};
哈希表,不过这里哈希表特殊的是,这是一个string对应一个数组,key对应一个数组value;
ans也是二维的,存放每一个value对应的数组
LeetCode 49 字母异位词分组详解(排序 + 哈希一遍过)
这道题是哈希表的经典入门题,核心思想其实非常简单:把"长得一样"的字符串归为一类 ;但这里的"长得一样"不是字面一样,而是排序后一样。
比如 "eat"、"tea"、"ate",排序之后都会变成 "aet";所以我们可以把 "aet" 作为一个"标识",所有排序后相同的字符串就归到同一组。
你的代码实现就是这个思路,而且是最主流写法。
先定义一个 unordered_map<string, vector<string>>,key 是排序后的字符串,value 是这一组的所有原字符串;然后遍历原数组,每拿到一个字符串,就复制一份出来排序,得到 key;接着把原字符串丢进对应的桶里;最后把 map 里的所有 value 收集起来,就是答案。
核心代码逻辑其实可以浓缩成一句话:
排序后的字符串作为 key,原字符串作为 value 进行分组。
你这段代码是完全正确的,我们稍微帮你拆一下关键点:
第一步,排序构造"特征值":
string s = strs[i];
sort(s.begin(), s.end());
这里 s 就是当前字符串的"标准形态"。
第二步,哈希分组:
hashmap[s].push_back(strs[i]);
如果这个 key 不存在,unordered_map 会自动创建一个空 vector,然后再 push。
第三步,收集答案:
cpp
for(auto it = hashmap.begin(); it != hashmap.end(); it++){
ans.emplace_back(it->second);
}
时间复杂度是 O(n * k log k),其中 n 是字符串个数,k 是单个字符串长度(排序的复杂度);空间复杂度是 O(nk)。
再补充一个进阶优化思路:其实可以不用排序,而是用"字符计数"作为 key,比如用一个长度为 26 的数组记录每个字母出现次数,然后拼成字符串作为 key,这样复杂度可以降到 O(nk),不过写起来稍微麻烦一点。
总结一下这题的套路:
分组问题 → 找"统一特征" → 用哈希表归类 ;
而"排序作为特征"是最常见、最稳的做法,这一类题基本都是这个思路。
这题属于面试高频,建议记住这个模板,后面很多字符串题都会用到类似思想。
3.

cpp
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
if (nums.empty()) return 0;
unordered_set<int> mp(nums.begin(), nums.end());
int longest = 0;
for (int num : mp) {
// 只从起点开始统计
if (!mp.count(num - 1)) {
int current = num;
int len = 1;
while (mp.count(current + 1)) {
current++;
len++;
}
longest = max(longest, len);
}
}
return longest;
}
};
最长连续序列(哈希优化)深入理解:为什么一定要找"起点"
你这份代码已经是这道题的最优解写法 ,接下来关键不是"会写",而是要真正理解:为什么这样写是 O(n)。
一、核心代码再看一遍
cpp
unordered_set<int> mp(nums.begin(), nums.end());
int longest = 0;
for (int num : mp) {
// 关键:只从起点开始
if (!mp.count(num - 1)) {
int current = num;
int len = 1;
while (mp.count(current + 1)) {
current++;
len++;
}
longest = max(longest, len);
}
}
二、最关键的一行代码
if (!mp.count(num - 1))
这句话的含义是:
只有当 num 是"连续序列的起点"时,才开始往后扩展
三、什么是"起点"
定义非常简单:
如果一个数 x 存在,但 x-1 不存在,那么它就是起点
举个例子:
nums = [100, 4, 200, 1, 3, 2]
转成集合后:
{1,2,3,4,100,200}
起点是:
-
1(没有0)
-
100(没有99)
-
200(没有199)
而 2、3、4 都不是起点,因为它们前面有数。
四、为什么必须这样写(核心)
如果你不加起点判断,会发生什么?
例如:
[1,2,3,4,5]
你会这样做:
-
从 1 开始 → 扫一遍(长度 5)
-
从 2 开始 → 又扫一遍(长度 4)
-
从 3 开始 → 再扫一遍(长度 3)
-
...
总复杂度:
O(n²)
五、加了"起点判断"之后
现在只会:
-
从 1 开始扫一次
-
2、3、4、5 全部跳过
每个元素最多被访问两次:
-
一次用于判断是不是起点
-
一次在 while 中被访问
所以总复杂度:
O(n)
六、这题真正的思维提升
这题最重要的不是代码,而是这个思想:
避免重复计算 → 只从"关键点"出发
这是一类非常重要的优化套路,在很多题里都会出现,比如:
-
区间问题
-
图遍历优化
-
哈希剪枝
七、一句话总结
连续问题 + O(n) 要求 = 哈希 + 起点扩展
如果你能完全理解"为什么只从起点开始",那这题你已经不是会做,而是吃透了。