提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、参考答案
-
- [1. `twoSum`(暴力解法)](#1.
twoSum(暴力解法)) - [2. `twoSum`(一次遍历哈希表)](#2.
twoSum(一次遍历哈希表)) -
- 核心思想:把哈希表当成"记忆本"
- [步骤化拆解(以`nums = [2,7,11,15]`,`target = 9`为例)](#步骤化拆解(以
nums = [2,7,11,15],target = 9为例)) - [再举一个重复元素的例子(`nums = [3,3]`,`target = 6`)](#再举一个重复元素的例子(
nums = [3,3],target = 6)) - 总结规律
- [1. `twoSum`(暴力解法)](#1.
- 二、思路解析
-
- [1. 为什么会想到用哈希表解两数之和?](#1. 为什么会想到用哈希表解两数之和?)
- [2. 哈希表解两数之和的核心思路 & 标准解题过程](#2. 哈希表解两数之和的核心思路 & 标准解题过程)
- [3. 同类问题:数组+哈希表解决的典型场景](#3. 同类问题:数组+哈希表解决的典型场景)
- [4. 这类问题的通用解题套路](#4. 这类问题的通用解题套路)
- [5. 示例:用套路解"和为K的子数组"(前缀信息类)](#5. 示例:用套路解“和为K的子数组”(前缀信息类))
- 三、胡思乱想
-
- [1. 两次遍历哈希表](#1. 两次遍历哈希表)
- [2. 用vector代替unordered_map](#2. 用vector代替unordered_map)
前言

一、参考答案
1. twoSum(暴力解法)
逻辑 :两层嵌套循环,遍历所有i < j的组合,找到和为target的下标对后直接返回。
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)(两层循环, n n n为数组长度)。
- 空间复杂度: O ( 1 ) O(1) O(1)(仅用了一个结果数组,无额外空间)。
- 缺点:数据量大时效率极低,因为嵌套循环的时间复杂度是平方级。
cpp
#include <vector>
using namespace std;
std::vector<int> twoSum(std::vector<int>& nums, int target)
{
for (int i = 0; i < nums.size(); ++i)
{
for (int j = i+1; j < nums.size(); ++j)
{
if (nums[i] + nums[j] == target)
{
return {i,j};
}
}
}
return {};
}
2. twoSum(一次遍历哈希表)
逻辑 :遍历数组时,先计算diff = target - nums[i],检查diff是否在哈希表中(此时哈希表仅存储当前元素之前的元素 );若存在则直接返回下标,若不存在则将当前元素的数值-下标存入哈希表。
- 示例验证 :
还是nums = [3,2,4],target = 6:i=0:diff=3,哈希表为空→存入{3:0}。i=1:diff=4,哈希表无4→存入{3:0, 2:1}。i=2:diff=2,哈希表有2:1→返回{1,2}(正确)。
cpp
#include <vector>
#include <unordered_map>
using namespace std;
vector<int> twoSum(vector<int>& nums, int target)
{
unordered_map<int, int> numMap; // key: 数值,value: 下标
for (int i = 0; i < nums.size(); ++i)
{
int diff = target - nums[i];
if (numMap.find(diff) != numMap.end())
{
return {numMap[diff], i};
}
numMap[nums[i]] = i;
}
return {}; // 题目保证有解,此处仅为语法兼容
}
核心思想:把哈希表当成"记忆本"
遍历数组时,我们的目标是找到两个数a + b = target。对于当前遍历的数b = nums[i],我们只需要知道之前是否见过a = target - b------哈希表就是用来记录"之前见过的数和它的下标"的记忆本。
步骤化拆解(以nums = [2,7,11,15],target = 9为例)
- 初始化 :空的记忆本(
numMap),结果数组为空。 - 遍历
i=0,当前数2:- 计算需要找的数:
9 - 2 = 7。 - 查记忆本:里面没有
7,说明之前没见过。 - 记到记忆本:把
2和下标0存进去(numMap = {2:0})。
- 计算需要找的数:
- 遍历
i=1,当前数7:- 计算需要找的数:
9 - 7 = 2。 - 查记忆本:里面有
2,对应的下标是0。 - 直接返回:
{0, 1}(找到答案,结束循环)。
- 计算需要找的数:
再举一个重复元素的例子(nums = [3,3],target = 6)
i=0,当前数3:- 找
6-3=3,记忆本空→存入{3:0}。
- 找
i=1,当前数3:- 找
6-3=3,记忆本有3:0→返回{0,1}。
- 找
总结规律
你可以把循环中的操作固定为三步口诀:
- 算差值(我需要找什么数?);
- 查记忆(之前见过这个数吗?);
- 存当前(没见过就把当前数记下来,继续走)。
这样一来,循环中的if判断只是"查记忆"的结果,找到就返回,没找到就执行存储,逻辑链条非常清晰,就不会再卡壳了。
二、思路解析
1. 为什么会想到用哈希表解两数之和?
核心是解决暴力解法的效率痛点,以及哈希表的"天然适配性":
暴力解法的核心问题
两数之和的暴力思路是:对每个元素nums[i],遍历剩下的元素找target - nums[i](互补数)。这个过程中,"找互补数"的操作要遍历O(n)个元素,导致整体时间复杂度O(n²)------数据量大时(比如n=10⁵)完全无法用。
哈希表的核心优势
哈希表(如C++的unordered_map/unordered_set)的核心特性是:以O(1)的平均时间复杂度完成"存在性查询"和"键值映射"。
- 我们需要的是"快速找到互补数是否存在,且拿到它的下标"------哈希表正好能把"数值→下标"做映射,且查值的成本从O(n)降到O(1)。
- 这是典型的"空间换时间":用O(n)的额外空间(存储哈希表),把时间复杂度从O(n²)降到O(n)。
直觉来源
当问题需要"快速判断某个元素是否存在"或"快速查找元素的关联信息(如下标、频次)"时,哈希表就是首选工具。两数之和的本质是"找配对",而配对的关键是"查互补数"------哈希表刚好能解决这个查值的痛点。
2. 哈希表解两数之和的核心思路 & 标准解题过程
核心思路
把"找两个数之和为target"转化为"对每个数nums[i],找是否存在互补数diff = target - nums[i]";
用哈希表记录"已遍历元素的数值→下标",遍历到nums[i]时,先查diff是否在哈希表中(即之前是否见过这个互补数):
- 若存在:直接返回"互补数的下标"和"当前下标i";
- 若不存在:把当前数
nums[i]和下标i存入哈希表,继续遍历。
标准解题过程(结构化步骤)
以nums = [2,7,11,15],target = 9为例:
| 步骤 | 操作 | 哈希表状态 | 结果 |
|---|---|---|---|
| 初始化 | 创建空的unordered_map<int, int> |
{} |
- |
| 遍历i=0(nums[i]=2) | 计算diff=9-2=7; 查哈希表:无7; 存入{2:0} | {2:0} |
- |
| 遍历i=1(nums[i]=7) | 计算diff=9-7=2; 查哈希表:有2,对应下标0; 返回{0,1} | {2:0} |
找到答案,结束 |
关键逻辑:哈希表只存"已遍历过的元素",因此互补数的下标一定在当前i之前,不会出现"取自身"的bug,且找到答案后可立即终止遍历。
3. 同类问题:数组+哈希表解决的典型场景
两数之和是"哈希表解决数组配对/查找问题"的入门题,这类问题有明确的共性:需要快速查询元素是否存在、或快速获取元素的关联信息(下标/频次),核心是"用哈希表的O(1)查找替代暴力遍历"。
以下是高频同类问题的分类总结:
| 问题类别 | 核心特征 | 典型例题 | 哈希表的作用 |
|---|---|---|---|
| 🔑 互补/配对类 | 找满足"两数运算关系(和/差/积)=目标值"的元素对 | 1. 两数之和(基础) 2. 两数之差等于目标值 3. 两数乘积等于目标值 4. 三数之和(优化版:固定一个数,转两数之和) | 记录已遍历元素的"值→下标",快速查互补数是否存在 |
| 📊 频次统计类 | 统计数组中元素的出现次数/位置,基于频次做判断 | 1. 多数元素(找出现次数>n/2的元素) 2. 只出现一次的数字(其余出现两次) 3. 两个数组的交集(统计第一个数组的频次,遍历第二个数组查频次) 4. 数组中出现次数最多的k个元素 | 用unordered_map<值, 频次>统计,避免重复遍历计数 |
| ✅ 存在性判断类 | 快速判断元素是否存在/是否重复/是否满足区间条件 | 1. 存在重复元素(判断数组是否有重复) 2. 存在重复元素II(判断是否有nums[i]=nums[j]且 | i-j |
| 📌 前缀信息类(子数组/子串) | 找满足条件的子数组/子串,依赖"前缀和/前缀状态" | 1. 和为K的子数组(前缀和+哈希表统计频次) 2. 最长无重复字符的子串(记录字符最后出现的下标) 3. 连续的子数组和(前缀和模k,查是否有相同余数) | 用哈希表记录"前缀和→出现次数"或"字符→最后下标",把子数组问题转化为前缀信息的查询 |
4. 这类问题的通用解题套路
无论具体场景如何,哈希表解决数组问题的核心套路是:
- 明确"键值对" :确定哈希表的
key和value(比如"值→下标""值→频次""前缀和→次数"); - 边遍历边操作:遍历数组时,先"查哈希表"(判断是否满足条件),再"更新哈希表"(存入当前元素/前缀信息);
- 提前终止:找到答案后立即返回,避免无效遍历;
- 空间换时间:接受O(n)的空间复杂度,换取O(n)的时间复杂度(远优于暴力的O(n²))。
5. 示例:用套路解"和为K的子数组"(前缀信息类)
题目:给定一个整数数组和一个整数k,找到该数组中和为k的连续子数组的个数。
- 键值对:
key=前缀和,value=该前缀和出现的次数; - 遍历过程:
① 初始化前缀和preSum=0,哈希表map={0:1}(处理前缀和刚好等于k的情况);
② 遍历数组,preSum += nums[i];
③ 查哈希表:若preSum - k存在,说明有map[preSum - k]个子数组和为k,累加计数;
④ 更新哈希表:map[preSum]++; - 最终返回计数结果。
这个过程和两数之和的"先查后存"逻辑完全一致,只是哈希表的键值对从"值→下标"变成了"前缀和→频次"------核心思路完全复用。
哈希表解两数之和的本质是"快速查互补数",而这类问题的核心是"用哈希表的O(1)查找替代暴力遍历"。只要遇到数组问题中需要"找配对、统计频次、判断存在性、查前缀信息"的场景,都可以优先考虑哈希表,且解题套路高度复用:明确键值对 → 边遍历边查 → 边查边更。
三、胡思乱想
1. 两次遍历哈希表
第一次刷算法题,除了暴力解算,哈希表算法根本也想不到,哈希表这种数据结构结束也少,红黑树、哈希表都是看过点视频,工作写垃圾代码就当作一个存键值对容器用,根本没啥子深入体会
看遍历数组时哈希表查询、哈希表插入,脑袋思路很不适应,总想着费点时间复杂度让代码看起来更朴素
然后写了这个bug实现
cpp
std::vector<int> twoSum2(std::vector<int>& nums, int target)
{
std::unordered_map<int, int> numMap; // key: 数值,value: 下标
for (int i = 0; i < nums.size(); ++i)
{
numMap[nums[i]] = i;
}
for (int i = 0; i < nums.size(); ++i)
{
int diff = target - nums[i];
if (numMap.find(diff) != numMap.end())
{
return { numMap[diff], i };
}
}
return {}; // 题目保证有解,此处仅为语法兼容
}
逻辑 :先遍历数组,将所有元素的数值-下标 存入哈希表(unordered_map);再遍历一次数组,计算diff = target - nums[i],检查diff是否在哈希表中,若存在则返回下标。
正确性 :存在严重bug。
- bug原因 :未判断找到的
diff对应的下标是否等于当前下标i,会出现取到自身下标的错误情况。 - 示例验证 :
比如nums = [3,2,4],target = 6:- 第一步:哈希表存储
{3:0, 2:1, 4:2}。 - 第二步遍历
i=0时,diff = 6-3=3,哈希表中存在3且下标为0(与i相同),此时会错误返回{0,0}(正确解应为{1,2})。
再比如nums = [1,3,4,2],target=8: - 哈希表存储
{1:0,3:1,4:2,2:3}。 - 遍历
i=2时,diff=4,哈希表中4的下标是2,会错误返回{2,2}。
- 第一步:哈希表存储
twoSum与twoSum2的核心提升点
twoSum是对twoSum2的逻辑修复+效率优化,提升点主要体现在以下四个维度:
| 维度 | twoSum2(两次遍历) | twoSum(一次遍历) |
|---|---|---|
| 正确性 | 存在"取自身下标"的bug,需额外加判断numMap[diff] != i才能修复 |
天然避免bug,无需额外判断(哈希表只存已遍历元素) |
| 时间效率 | 两次完整遍历数组(必须遍历完所有元素),时间复杂度 O ( n ) O(n) O(n)但常数项大 | 一次遍历(找到答案后立即返回,无需遍历剩余元素),常数项更小(实际运行更快) |
| 空间效率 | 必须存储所有元素到哈希表(最坏/最好情况都是 O ( n ) O(n) O(n)) | 仅存储"到答案为止的元素"(最好情况 O ( 1 ) O(1) O(1),最坏情况 O ( n − 1 ) O(n-1) O(n−1)),空间利用率更高 |
| 代码简洁性 | 分两次遍历,逻辑拆分,代码稍冗余 | 一次遍历完成"查找+存储",逻辑紧凑,代码更简洁 |
关键优化逻辑:从"先存后查"到"先查后存"
twoSum2的思路是先把所有元素存进哈希表,再逐个查 ,相当于"先建库,再查库";
twoSum的思路是边存边查,查的是已经存好的部分,相当于"建库的同时查库,查到就停"。
这种思路的转变是核心:
twoSum2的哈希表包含所有元素,因此需要额外判断"是否是自己";twoSum的哈希表只包含当前元素之前的元素,因此永远不会查到自己,且能提前终止遍历。
修复后的twoSum2(可选补充)
如果要修复twoSum2的bug,需要在判断时加上numMap[diff] != i:
cpp
std::vector<int> twoSum2_fixed(std::vector<int>& nums, int target)
{
std::unordered_map<int, int> numMap;
for (int i = 0; i < nums.size(); ++i)
{
numMap[nums[i]] = i;
}
for (int i = 0; i < nums.size(); ++i)
{
int diff = target - nums[i];
if (numMap.find(diff) != numMap.end() && numMap[diff] != i) // 加判断
{
return { numMap[diff], i };
}
}
return {};
}
但即使修复后,它的效率依然不如twoSum(需要两次遍历),因此twoSum是最优解。
2. 用vector代替unordered_map
如果已遍历过的元素不是采用哈希表存储也是用一个vector
存储,那时间复杂度是不是就是暴力结算一样了,这里用到哈希表就是查询时间复杂度低的原因?
判断完全正确 !这正是哈希表解决两数之和的核心价值------用哈希表的O(1)平均查询效率,替代线性容器(如vector)的O(n)查询效率,从而把整体时间复杂度从O(n²)降到O(n)。
用vector存储已遍历元素:时间复杂度退化为暴力级别
我们先模拟"用vector存已遍历元素"的解题流程,对比时间复杂度:
cpp
std::vector<int> twoSum_vector(std::vector<int>& nums, int target) {
std::vector<std::pair<int, int>> visited; // 存<数值, 下标>,模拟已遍历元素
for (int i = 0; i < nums.size(); ++i) {
int diff = target - nums[i];
// 关键:查询diff是否在visited中 → 必须遍历vector
for (auto& p : visited) {
if (p.first == diff) {
return {p.second, i};
}
}
// 没找到则存入当前元素
visited.emplace_back(nums[i], i);
}
return {};
}
时间复杂度分析
- 外层循环:遍历nums,共n次(n为数组长度);
- 内层循环:每次查询diff时,需要遍历
visited(已遍历的元素数,最多i个); - 总操作次数:1 + 2 + 3 + ... + (n-1) = n(n-1)/2 → 时间复杂度为O(n²),和暴力解法(两层循环)完全一致。
核心问题
vector是线性存储结构,没有"索引/哈希映射",要判断某个值是否存在,只能逐个遍历比对------这和暴力解法中"遍历剩余元素找互补数"本质上是同一回事,只是遍历的顺序(已遍历 vs 未遍历)不同,效率没有任何提升。
哈希表的核心优势:查询效率的质变
哈希表(如unordered_map/unordered_set)的底层是哈希桶结构,其核心是通过"哈希函数"将键(如数值)直接映射到内存地址,查询时:
- 用哈希函数计算键对应的哈希值 → 定位到对应的哈希桶;
- 若桶内无冲突(绝大多数情况),直接拿到值 → 平均时间复杂度O(1);
- 即使有冲突(如哈希碰撞),也只需遍历桶内少量元素,最坏O(n)(实际工程中几乎不会出现)。
效率对比(直观举例)
假设nums长度n=10000:
- 用vector存储:最坏情况下,最后一个元素需要遍历9999个已遍历元素 → 总操作约5000万次;
- 用哈希表存储:每个元素的查询操作仅需1次 → 总操作约1万次;
- 差距达到5000倍,数据量越大,差距越悬殊。
关键结论:哈希表的价值是"O(1)查询",而非"存储"
| 维度 | vector存储已遍历元素 | 哈希表存储已遍历元素 | 纯暴力解法 |
|---|---|---|---|
| 存储逻辑 | 线性存储<值, 下标> | 哈希映射<值, 下标> | 无存储 |
| 查询时间 | O(n)(遍历比对) | O(1)(哈希映射) | O(n)(遍历剩余元素) |
| 整体时间复杂度 | O(n²) | O(n) | O(n²) |
| 空间复杂度 | O(n)(存已遍历元素) | O(n)(存已遍历元素) | O(1) |
可以清晰看到:
- 哈希表和vector的空间复杂度相同(都是O(n)),但查询效率天差地别;
- 哈希表的"空间换时间",换的不是"存储的空间",而是"查询的时间"------用O(n)的空间,把查询的O(n)降到O(1),最终让整体时间复杂度从O(n²)降到O(n)。
延伸:所有"线性容器查询"都会退化为暴力级别
不只是vector,只要是无索引的线性结构(如list、deque),用来存储已遍历元素时,查询操作都需要线性遍历,时间复杂度都会退化为O(n²):
- list:查询需要逐个遍历节点,O(n);
- deque:虽然支持随机访问,但判断"值是否存在"仍需遍历,O(n);
- 只有哈希表(unordered_map/unordered_set)、有序容器(map/set,查询O(logn))能突破线性查询的限制。
总结
你抓准了核心:
- 用vector存已遍历元素,查询互补数的过程仍是O(n),整体时间复杂度和暴力解法一致;
- 哈希表的核心价值是O(1)的平均查询效率,这是它能把两数之和的时间复杂度从O(n²)降到O(n)的根本原因;
- "空间换时间"的本质:不是单纯多占用空间,而是用空间换"查询效率的质变"。