「两数之和」是 LeetCode 中非常经典的一道数组题,主要考察数组遍历、哈希表 Map、差值查找以及时间复杂度优化。
这道题最重要的思路是:将寻找两个数的问题,转换成寻找另一个数是否存在的问题。
题目描述
给定一个整数数组 nums 和一个目标值 target,要求找到数组中两个数字,使它们相加等于 target,并返回对应的下标。
例如:
nums = [2, 7, 11, 15]
target = 9
因为:
nums[0] + nums[1] === 9
// 2 + 7 = 9
所以返回:
[0, 1]
题目还有两个重要条件:每种输入保证只有一个有效答案,且不能重复使用同一个位置的元素。
因此,只要找到符合条件的一组下标,就可以立即返回。
暴力解法:双重 for 循环
最直接的思路是依次选择两个数字,判断它们的和是否等于 target。
var twoSum = function(nums, target) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] === target) {
return [i, j]
}
}
}
}
外层循环 i 负责选择第一个数字,内层循环 j 负责选择第二个数字。
这里需要注意:
let j = i + 1
之所以从 i + 1 开始,是为了避免使用同一个数组元素,同时也不需要重复比较已经检查过的组合。
例如:
nums = [2, 7, 11, 15]
target = 9
第一次比较:
nums[0] + nums[1]
// 2 + 7 = 9
满足条件,直接:
return [0, 1]
这种方式简单直观,但最坏情况下需要双重遍历,时间复杂度为 O(n²) ,额外空间复杂度为 O(1)。
优化思路:利用 Map 查找差值
暴力解法之所以效率较低,是因为每选择一个数字,都需要再次遍历数组寻找另一个数字。
其实可以换一种思路。
假设:
target = 9
nums[i] = 2
那么另一个数字必须是:
target - nums[i]
// 9 - 2 = 7
也就是说,我们不需要不断尝试两个数字相加,只需要判断:
当前数字所需要的另一个数字,之前是否出现过?
这时就可以使用 Map 哈希表。
Map 是什么?
Map 是 JavaScript 提供的键值对集合,可以通过 key 快速找到对应的 value。
例如:
const map = new Map()
map.set(2, 0)
map.set(7, 1)
这里保存的是:
数字 → 下标
2 → 0
7 → 1
常用方法如下:
| 方法 | 作用 |
|---|---|
new Map() |
创建 Map |
map.set(key, value) |
保存键值对 |
map.has(key) |
判断键是否存在 |
map.get(key) |
获取键对应的值 |
例如:
map.has(2) // true
map.get(2) // 0
在这道题中,使用 Map 保存已经遍历过的数字和对应下标,就能避免重复遍历数组。
Map 优化后的完整代码
javascript
var twoSum = function(nums, target) {
const map = new Map()
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i]
if (map.has(need)) {
return [map.get(need), i]
}
map.set(nums[i], i)
}
}
代码虽然很短,但包含三个关键操作。
首先:
const need = target - nums[i]
计算当前数字还需要哪个数字,才能凑出目标值。
接着:
if (map.has(need))
判断这个数字是否已经出现过。
如果存在,就通过:
map.get(need)
获取它之前保存的下标,然后和当前下标 i 一起返回。
如果不存在,就执行:
map.set(nums[i], i)
将当前数字和下标保存到 Map,供后续遍历使用。
结合示例理解执行过程
例如:
nums = [2, 7, 11, 15]
target = 9
刚开始:
map = new Map()
Map 为空。
当 i = 0 时:
nums[i] = 2
need = 9 - 2
// 7
检查:
map.has(7)
// false
由于 Map 中没有 7,所以保存当前数字:
map.set(2, 0)
此时 Map 保存:
2 → 0
当 i = 1 时:
nums[i] = 7
need = 9 - 7
// 2
再次检查:
map.has(2)
// true
说明之前已经遇到过 2。
通过:
map.get(2)
// 0
拿到之前的下标,再结合当前下标:
return [0, 1]
至此找到答案,函数执行结束。
为什么要先查找,再存入 Map?
注意代码顺序:
if (map.has(need)) {
return [map.get(need), i]
}
map.set(nums[i], i)
必须先查找,再存入当前数字,才能避免同一个元素被使用两次。
例如:
nums = [3, 3]
target = 6
第一次遍历时,Map 为空,先保存:
3 → 0
第二次遇到 3 时,才会找到之前的 3,因此返回:
[0, 1]
两个数字虽然相同,但下标不同,符合题意。
为什么找到答案就直接 return?
因为题目明确保证:
每种输入只会对应一个有效答案。
因此:
if (map.has(need)) {
return [map.get(need), i]
}
找到后就不需要继续遍历。
不过,如果题目变成:
nums = [2, 7, 3, 6]
target = 9
这里有两组符合条件的下标:
2 + 7 = 9 → [0, 1]
3 + 6 = 9 → [2, 3]
原来的代码只会返回第一组找到的答案。
如果题目要求返回所有符合条件的下标组合 ,就不能直接 return,而且需要调整 Map 的存储方式,避免遗漏同一个数字对应的多个下标。
这也是做算法题时必须注意的一点:不同的题目条件,可能需要不同的处理方式。
时间复杂度和空间复杂度
两种解法对比如下:
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 双重 for 循环 | O(n²) | O(1) |
| Map 哈希表 | O(n) | O(n) |
Map 解法只需要遍历数组一次,而 Map.has()、Map.get()、Map.set() 的平均操作时间可以看作 O(1),因此整体平均时间复杂度是 O(n)。
不过,Map 最坏情况下需要存储接近 n 个元素,因此额外空间复杂度是 O(n)。
也就是说,Map 解法通过增加空间消耗,减少了重复遍历的时间。
总结
这道题最重要的优化思想,是把寻找两个数的问题转换成查找差值的问题。
最终代码:
javascript
var twoSum = function(nums, target) {
const map = new Map()
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i]
if (map.has(need)) {
return [map.get(need), i]
}
map.set(nums[i], i)
}
}
整体执行流程:
遍历当前数字
↓
计算 need = target - nums[i]
↓
检查 Map 是否存在 need
↓
存在 → 返回之前下标和当前下标
↓
不存在 → 保存当前数字及其下标
↓
继续遍历
一句话记住:Map 保存已经遍历过的数字和下标,每遇到一个数字,就检查目标差值是否已经出现。
这道题不仅要掌握代码,更要理解其中的 空间换时间 思想,它也是哈希表在算法题中最经典的应用之一。