Grind 75 刷题第 1 题:Two Sum,两数之和到底该怎么想?
准备美国程序员面试时,很多人都会碰到 Grind 75。
它不是让你漫无目的地刷几百道题,而是从常见面试题中挑出一组有代表性的题目,帮助你集中练习数组、哈希表、链表、二叉树、动态规划等核心知识。
今天从最经典的一道题开始:
Two Sum,两数之和。
这道题看起来很简单,但它几乎包含了算法面试最常见的一种优化思路:
如何用额外的存储空间,减少重复查找。
题目
给定一个整数数组 nums 和一个整数 target,找出数组中两个数字,使它们相加等于 target。
返回这两个数字的数组下标。
假设每组输入只有一个答案,并且同一个元素不能使用两次。
例如:
nums = [2, 7, 11, 15] target = 9
因为:
nums[0] + nums[1] = 2 + 7 = 9
所以返回:
[0, 1]
注意,题目要求返回的是下标,不是数字本身。
第一反应:两个数字两两比较
最容易想到的方法,是用两层循环。
先拿第一个数字和后面的每个数字比较,再拿第二个数字和后面的数字比较。
def two_sum(nums, target): for i in range(len(nums)): for j in range(i + 1, len(nums)): if nums[i] + nums[j] == target: return [i, j]
这个方法可以得到正确答案,但时间复杂度是:
O(n²)
数组较小时没问题,但如果数组里有很多数字,就会进行大量重复查找。
面试官通常会继续问:
能不能只遍历一次数组?
优化思路:不要找两个数,只找当前数字缺少的那个数
假设当前遍历到数字 2,目标值是 9。
我们真正需要找的是:
9 - 2 = 7
这个 7 通常叫作 complement,也就是当前数字对应的“补数”。
计算公式是:
complement = target - nums[i]
接下来只需要判断:
这个 complement 之前有没有出现过?
如果出现过,说明已经找到了答案。
如果没有出现,就把当前数字和它的下标存入哈希表,继续遍历。

用一个例子走完整个流程
输入:
nums = [2, 7, 11, 15] target = 9
开始时,哈希表为空:
map = {}
第一步:指针来到下标 0
当前数字:
nums[0] = 2
计算 complement:
complement = 9 - 2 = 7
检查 7 是否在 map 中:
不在
所以先把当前数字 2 和它的下标 0 存进 map:
map = {
2: 0
}
继续下一轮循环。
第二步:指针来到下标 1
当前数字:
nums[1] = 7
计算 complement:
complement = 9 - 7 = 2
检查 2 是否在 map 中。
此时 map 是:
{
2: 0
}
发现 2 已经存在,并且它的下标是 0。
当前数字 7 的下标是 1。
所以直接返回:
[0, 1]
程序到这里结束,不需要继续检查后面的 11 和 15。
整个过程可以简化为:
i = 0 当前数字 2 需要寻找 7 map 中没有 7 把 2 和下标 0 存入 map ↓ i = 1 当前数字 7 需要寻找 2 map 中存在 2 返回 [0, 1]
Python 解法
def two_sum(nums, target):
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
这里的 seen 就是哈希表。
它保存的结构是:
数字: 下标
例如:
{
2: 0
}
表示数字 2 出现在数组下标 0 的位置。
JavaScript 解法
function twoSum(nums, target) {
const seen = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (seen.has(complement)) {
return [seen.get(complement), i];
}
seen.set(nums[i], i);
}
return [];
}
JavaScript 使用 Map 保存已经遍历过的数字和下标。
核心逻辑与 Python 完全相同。
为什么要先检查,再放进 map?
这是 Two Sum 最容易被忽略的地方。
例如:
nums = [3, 2, 4] target = 6
当指针来到第一个数字 3 时:
complement = 6 - 3 = 3
如果先把当前的 3 放进 map,再检查 complement,就可能错误地把同一个元素使用两次。
但题目规定:
同一个数组元素不能重复使用。
所以正确顺序应该是:
先检查 complement 是否在 map 中 再把当前数字放进 map
重复数字怎么办?
例如:
nums = [3, 3] target = 6
第一轮:
当前数字 = 3
complement = 3
map 中没有 3
存入 {3: 0}
第二轮:
当前数字 = 3 complement = 3 map 中已经有 3 返回 [0, 1]
所以哈希表方法也可以正确处理重复数字。
时间复杂度
数组只需要遍历一次。
哈希表的平均查询时间是 O(1),所以总时间复杂度是:
O(n)
因为最坏情况下需要把数组中的元素存入哈希表,所以空间复杂度是:
O(n)
与两层循环相比:
暴力解法:时间 O(n²),空间 O(1) 哈希表:时间 O(n),空间 O(n)
这就是典型的:
用空间换时间。
面试时怎么讲?
面试时可以先说暴力解法,再主动提出优化。
可以这样表达:
最直接的方法是使用两层循环,检查数组中的每一对数字,时间复杂度是 O(n²)。
为了减少重复查找,我可以使用哈希表保存已经遍历过的数字及其下标。遍历当前数字时,计算 target 减去当前数字得到 complement。如果 complement 已经在哈希表中,就返回 complement 的下标和当前下标。这样只需要遍历数组一次,时间复杂度是 O(n)。
这几句话基本就把 Two Sum 的核心讲清楚了。
这道题真正要记住什么?
不要死记代码,要记住这个判断过程:
当前数字是多少? ↓ 距离 target 还差多少? ↓ 这个差值以前出现过吗? ↓ 出现过:返回两个下标 没出现:保存当前数字和下标
以后看到这类题目时,可以优先考虑哈希表:
- 查找两个数字是否满足某种关系;
- 判断某个值之前是否出现过;
- 需要快速查询数组中的历史元素;
- 暴力解法存在大量重复查找。
今日刷题记录
题目:
Two Sum
核心数据结构:
Hash Map
核心公式:
complement = target - current number
时间复杂度:
O(n)
空间复杂度:
O(n)
最容易犯的错误:
先把当前数字存进 map,再检查 complement
正确顺序:
先检查,再存入
Grind 75 的第一道题并不难,但它建立了一个非常重要的刷题习惯:
不要只想着怎么得到答案,还要思考哪些重复操作可以被省掉。
评论
0 条正在加载评论...