1. 两数之和 (Easy)
专题归类: 01-哈希表 LeetCode 链接: https://leetcode.cn/problems/two-sum/
在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode
题目描述
给定一个整数数组 nums 和一个整数目标值 target,请在该数组中找出和为目标值的两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案(即一定有解,且解唯一)。但是,数组中同一个元素不能使用两遍。
示例:
输入:nums = [2, 7, 11, 15], target = 9
输出:[0, 1]
解释:因为 nums[0] + nums[1] == 9
补充说明:
- 数组长度范围:
2 <= nums.length <= 10^4 - 数值范围:
-10^9 <= nums[i] <= 10^9 - 保证有且仅有一组有效答案
题目详细分析
数据范围含义:
- 数组长度最大 10^4,O(n^2) 的暴力解法会达到 10^8 次操作,在 LeetCode 上会超时。因此需要 O(n) 或 O(n log n) 的解法。
- 数值范围达到 ±10^9,说明两数之和可能达到 ±2×10^9,在 32 位整数范围内没有问题,不需要考虑大数溢出。
- “同一个元素不能使用两遍”意味着不能自己加自己,例如
target=6, nums=[3]不能返回[0,0]。
输入输出特征:
- 输入是无序整数数组,返回的是下标数组(长度为 2)。
- 题目保证有唯一解,因此不需要处理无解的情况。
边界条件:
- 最短数组长度为 2,不存在不足 2 个元素的情况。
- 可能有负数,所以
target - num可能大于num本身。 - 可能有重复值,例如
nums = [3, 3], target = 6,返回[0, 1],但两个 3 是不同位置的。
隐藏条件:
- “唯一解”本身就是一个重要信息——找到后就可以立即返回,不需要继续遍历。
- 返回的是下标而非值,因此排序后不能直接返回原下标,需要记录映射关系。
小白版直白理解
想象你在参加一个派对,每个人身上贴着一个数字。主持人说:“谁身上的数字加起来等于 9?“你需要找到这两个人。
笨办法(暴力法): 你一个一个地问每一个人:“你身上数字是多少?“然后去问其他人有没有人和他加起来等于 9。这个办法很慢——如果派对有 100 个人,最多要问 100×100 = 10000 次。
聪明办法(哈希表法): 你准备一个记事本。每见到一个人,你在记事本上记下”要找 XX 号人”。比如第一个人身上是 2,你记下”需要找 7”。然后第二个人来了身上是 7,你翻开记事本一看——“需要找 7”,找到了!你们俩加起来就是 9。
这个方法只需要在派对上走一圈,每见一个人查一下记事本,O(n) 时间搞定。
解题思路
思路一:暴力枚举(不推荐)
最直接的想法:两层循环,枚举所有可能的组合,检查两数之和是否等于 target。
但时间复杂度 O(n
def twoSum(nums, target):
n = len(nums)
for i in range(n):
for j in range(i + 1, n):
if nums[i] + nums[j] == target:
return [i, j]思路二:两遍哈希表
第一遍遍历:将所有元素的值和下标存入哈希表(值 → 下标)。
第二遍遍历:对每个元素 nums[i],检查 target - nums[i] 是否在哈希表中,且下标不是 i。
需要额外处理”不能重复使用同一元素”的问题。
def twoSum(nums, target):
# 第一遍:建立值到索引的映射
mapping = {}
for i, num in enumerate(nums):
mapping[num] = i
# 第二遍:查找互补元素
for i, num in enumerate(nums):
complement = target - num
if complement in mapping and mapping[complement] != i:
return [i, mapping[complement]]思路三:一遍哈希表(推荐)
核心洞察:不需要把所有元素先存进去再查,可以边遍历边查找。
遍历到元素 num 时,检查 complement = target - num 是否已经出现在哈希表中:
- 如果出现过,说明之前遍历过的某个元素和当前元素正好凑成 target,直接返回。
- 如果没出现过,把当前元素存入哈希表,供后面的元素查。
这样做的好处是:一次遍历完成,且天然不会出现同一元素用两次的情况(因为当前元素还没存进哈希表)。
def twoSum(nums, target):
"""
一遍哈希表解法
- seen: 记录已遍历过的 {值: 下标}
- 对每个元素 num, 检查 target - num 是否已在 seen 中
"""
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i易错点
- 重复元素混淆: 如果
nums = [3, 3], target = 6,用两遍哈希表时,第二遍遍历到第一个 3,查到的 mapping[3] 是 1(第二个 3 覆盖了第一个),但检查 mapping[complement] != i 会跳过,导致漏解。一遍哈希表没有这个问题。 - 负数处理: target 可能是负数,complement = target - num 可能大于 num,不要假设 complement 一定比 num 小。
- 提前返回: 找到解后立即 return,不要继续循环,否则可能覆盖结果。
- 哈希表覆盖: 用两遍哈希表时,如果有重复元素,后面出现的会覆盖前面的下标。一遍哈希表不会遇到这个问题。
框架提炼
哈希表辅助查找模板:
当问题需要在遍历过程中”回头看”已处理过的元素时,可以用哈希表记录历史信息,将查找从 O(n) 降到 O(1)。
def xxx_problem(nums, target):
# 1. 初始化哈希表
seen = {}
# 2. 遍历元素
for i, num in enumerate(nums):
# 3. 检查互补条件是否满足
complement = need_to_find(num, target) # 不同问题不同计算
if complement in seen:
# 4. 找到了,处理结果
return process_result(seen[complement], i)
# 5. 记录当前元素供后续使用
seen[num] = i适用场景特征:
- 需要查找两元素之间的关系(和、差、积等)
- 可以边遍历边记录
- 只需要一次遍历就能得到结果
关联题目
- 15-三数之和 — 从两数之和扩展到三数之和,排序 + 双指针解决,去重成为新增的难点
- 18-四数之和 — 进一步扩展到四数之和,排序 + 双指针嵌套,剪枝优化更复杂
- 167-两数之和 II - 输入有序数组 — 同样是两数之和,但输入已排序,可以用双指针 O(1) 空间解决,不需要哈希表