【LeetCode 热题 HOT 100】两数之和

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言


一、参考答案

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=0diff=3,哈希表为空→存入{3:0}
    • i=1diff=4,哈希表无4→存入{3:0, 2:1}
    • i=2diff=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为例)

  1. 初始化 :空的记忆本(numMap),结果数组为空。
  2. 遍历i=0,当前数2
    • 计算需要找的数:9 - 2 = 7
    • 查记忆本:里面没有7,说明之前没见过。
    • 记到记忆本:把2和下标0存进去(numMap = {2:0})。
  3. 遍历i=1,当前数7
    • 计算需要找的数:9 - 7 = 2
    • 查记忆本:里面有2,对应的下标是0
    • 直接返回:{0, 1}(找到答案,结束循环)。

再举一个重复元素的例子(nums = [3,3]target = 6

  1. i=0,当前数3
    • 6-3=3,记忆本空→存入{3:0}
  2. i=1,当前数3
    • 6-3=3,记忆本有3:0→返回{0,1}

总结规律

你可以把循环中的操作固定为三步口诀

  1. 算差值(我需要找什么数?);
  2. 查记忆(之前见过这个数吗?);
  3. 存当前(没见过就把当前数记下来,继续走)。

这样一来,循环中的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. 这类问题的通用解题套路

无论具体场景如何,哈希表解决数组问题的核心套路是:

  1. 明确"键值对" :确定哈希表的keyvalue(比如"值→下标""值→频次""前缀和→次数");
  2. 边遍历边操作:遍历数组时,先"查哈希表"(判断是否满足条件),再"更新哈希表"(存入当前元素/前缀信息);
  3. 提前终止:找到答案后立即返回,避免无效遍历;
  4. 空间换时间:接受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}

twoSumtwoSum2的核心提升点

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)的底层是哈希桶结构,其核心是通过"哈希函数"将键(如数值)直接映射到内存地址,查询时:

  1. 用哈希函数计算键对应的哈希值 → 定位到对应的哈希桶;
  2. 若桶内无冲突(绝大多数情况),直接拿到值 → 平均时间复杂度O(1)
  3. 即使有冲突(如哈希碰撞),也只需遍历桶内少量元素,最坏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)的根本原因;
  • "空间换时间"的本质:不是单纯多占用空间,而是用空间换"查询效率的质变"。

相关推荐
Tisfy2 小时前
LeetCode 2054.两个最好的不重叠活动:二分查找
算法·leetcode·二分查找·题解
Looooking2 小时前
Python 之通过一个天平找出9个小球中唯一重量较轻的小球
python·算法
white-persist2 小时前
【攻防世界】reverse | tt3441810 详细题解 WP
java·c语言·开发语言·数据结构·c++·算法·安全
YGGP2 小时前
【Golang】LeetCode 70. 爬楼梯
算法·leetcode
小熳芋2 小时前
组合总和- python-回溯哦&剪枝
算法·机器学习·剪枝
lxh01132 小时前
缺失的第一个正数
数据结构·算法
啊阿狸不会拉杆2 小时前
《数字图像处理》第 12 章 - 图像模式分类
图像处理·人工智能·算法·机器学习·计算机视觉·分类·数据挖掘
LYFlied2 小时前
【每日算法】LeetCode 763. 划分字母区间(贪心算法)
前端·算法·leetcode·面试·贪心算法