今天拆解 LeetCode 入门必刷第一题——两数之和,这道题看似简单,却能帮我们理清双指针、哈希表两种核心解题思路,还能避开不少新手容易踩的坑。话不多说,先看题目!
一、题目描述(LeetCode 1. 两数之和)
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
📌 关键约束:
-
每种输入只会对应一个答案(不用考虑多解情况);
-
不能使用两次相同的元素(即同一个下标对应的元素不能用两次);
-
答案可以按任意顺序返回。
💡 示例:
输入:nums = [2,7,11,15], target = 9 → 输出:[0,1](因为 2+7=9,对应下标 0 和 1)
输入:nums = [3,2,4], target = 6 → 输出:[1,2](注意不是 [0,0],不能重复使用元素)
二、解法一:双指针解法(时间复杂度 O(n log n),需处理索引问题)
1. 解题思路
核心思路:排序 + 双指针。先将数组排序,再用左右两个指针分别指向数组两端,通过调整指针位置找到和为 target 的两个元素,最后返回它们的原始下标。
步骤拆解:
-
保存原始索引:因为排序会打乱数组的原始下标,所以先创建一个包含「元素值 + 原始下标」的对象数组;
-
排序:对新数组按元素值从小到大排序;
-
双指针遍历:左指针(left)指向开头(最小元素),右指针(right)指向结尾(最大元素),计算两元素之和:
-
和 = target:找到答案,返回两个元素的原始下标;
-
和 < target:左指针右移(增大元素值,让和变大);
-
和 > target:右指针左移(减小元素值,让和变小)。
2. 完整代码(TypeScript)
function twoSum_2(nums: number[], target: number): number[] {
// 1. 创建包含数值和原始索引的数组,避免排序打乱原始索引
const numsWithIndex = nums.map((num, index) => ({ num, index }));
// 2. 按数值大小排序(仅排序新数组,不修改原数组)
numsWithIndex.sort((a, b) => a.num - b.num);
// 3. 初始化双指针
let left = 0;
let right = numsWithIndex.length - 1;
// 4. 双指针遍历,寻找和为 target 的两个元素
while (left < right) {
const sum = numsWithIndex[left].num + numsWithIndex[right].num;
if (sum === target) {
// 返回原始索引(不是排序后的指针位置!)
return [numsWithIndex[left].index, numsWithIndex[right].index];
} else if (sum < target) {
// 和偏小,左指针右移,增大数值
left++;
} else {
// 和偏大,右指针左移,减小数值
right--;
}
}
// 题目约束有唯一解,此处仅为语法兼容
return [];
}
3. 关键细节 & 避坑点
-
⚠️ 新手最容易踩的坑:直接排序原数组!排序会打乱原始下标,导致最后返回的指针位置不是题目要求的「原始下标」,所以必须先保存原始索引;
-
排序的时间复杂度:数组排序的时间复杂度是 O(n log n),后续双指针遍历是 O(n),整体时间复杂度由排序决定;
-
双指针的优势:空间复杂度低(O(n),仅用于保存索引数组),思路直观,适合对空间复杂度有要求,但对时间复杂度要求不极致的场景。
三、解法二:哈希表(Map)解法(最优解,时间复杂度 O(n))
1. 解题思路
核心思路:用空间换时间,遍历数组时,记录每个元素的「值」和「下标」,同时判断「目标值 - 当前元素」是否已经在哈希表中。
举个例子:target = 9,当前元素是 2(下标 0),那么我们需要找 9-2=7。此时哈希表中没有 7,就把 2 和下标 0 存入哈希表;继续遍历到 7(下标 1),找 9-7=2,发现 2 已经在哈希表中,直接返回 [2 的下标, 7 的下标] 即可。
优势:只需遍历一次数组,时间复杂度从暴力解法的 O(n²) 降到 O(n),是这道题的最优解,弥补了双指针解法时间复杂度较高的不足。
2. 完整代码(TypeScript)
function twoSum_1(nums: number[], target: number): number[] {
// 定义 Map,key 存数组元素值,value 存对应下标
const map = new Map<number, number>();
// 遍历数组,i 是当前元素下标,nums[i] 是当前元素值
for (let i = 0; i < nums.length; i++) {
// 计算需要找的互补数(目标值 - 当前元素)
const complement = target - nums[i];
// 判断互补数是否已经在 Map 中(存在即找到答案)
if (map.has(complement)) {
// 返回互补数的下标 和 当前元素下标
return [map.get(complement)!, i];
}
// 若不存在,将当前元素和下标存入 Map,供后续查找
map.set(nums[i], i);
}
// 题目约束有唯一解,这里仅为语法兼容,实际不会执行
return [];
};
3. 关键细节 & 避坑点
-
Map 的作用:快速查询「互补数」是否存在,查询时间复杂度是 O(1),这是提升效率的核心;
-
为什么先判断再存入?避免「重复使用同一个元素」。比如 nums = [3,3], target = 6,遍历第一个 3 时,Map 中没有 3,存入;遍历第二个 3 时,找 6-3=3,此时 Map 中有 3(第一个 3 的下标),直接返回 [0,1],不会出现重复使用的情况;
-
Map.get(complement)! 中的「!」:TypeScript 语法,告诉编译器 complement 一定在 Map 中(题目约束有唯一解),避免编译报错。
四、两种解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 核心优势 | 适用场景 |
|---|---|---|---|---|
| 双指针 | O(n log n) | O(n) | 空间利用率高,思路直观 | 对空间有要求,或后续需要排序的延伸场景 |
| 哈希表(Map) | O(n) | O(n) | 效率最高,一次遍历搞定 | 大多数情况(推荐首选) |
五、总结
两数之和作为 LeetCode 入门题,核心考察「双指针的区间调整」和「哈希表的快速查询」两种思路,重点记住:
-
双指针解法要注意「保存原始索引」,避免排序打乱下标,时间复杂度由排序决定;
-
哈希表解法是最优解,时间复杂度 O(n),关键是「用 Map 保存元素和下标,找互补数」;
-
解题时先看题目约束(如“唯一解”“不重复使用元素”),这些约束会帮我们简化逻辑、避开坑。