一、题目与核心考点
题目要求
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案,且同一个元素不能使用两次。你可以按任意顺序返回答案。
核心考点
这道题本质考察「查找效率的优化」,核心能力点:
- 暴力枚举的基础遍历逻辑,避免重复配对
- 哈希表的 O (1) 查找特性,实现空间换时间
- 边界防御与遍历顺序控制,保证逻辑严谨
最优解时间复杂度 O (n),空间复杂度 O (n),是典型的空间换时间方案。
二、思路完整推导:从暴力到最优
1. 入门思路:暴力枚举两两配对
最直观的解法非常容易想到:遍历数组中所有可能的数对,判断它们的和是否等于 target。
- 外层循环固定第一个数的下标
i - 内层循环从
i+1开始,枚举第二个数的下标j - 逐一判断
nums[i] + nums[j] == target,命中则返回下标
内层循环从 i+1 开始,有两个好处:
- 不会出现
(i,j)和(j,i)重复判断同一对数 - 不会用到同一个下标的元素两次,符合题目要求
但暴力法有一个明显的缺点:双重循环嵌套,时间复杂度是 O (n²),数组规模较大时会严重超时。
2. 发现瓶颈:查找另一半的效率太低
暴力法慢的根源是什么? 对于每一个 nums[i],我们要找的其实是 target - nums[i] 这个「差值」有没有在数组里出现过。暴力法用了一次循环去查找这个差值,每次查找都是 O (n),所以整体才会是 O (n²)。
那么问题就转化成了:能不能用更快的方式,查到「某个值有没有出现过、它的下标是多少」?
答案就是哈希表。哈希表的查找操作平均时间复杂度是 O (1),刚好完美解决查找慢的问题。
3. 最优解:哈希表一次遍历
核心思路:遍历数组的同时,用哈希表记录已经走过的元素,边遍历边查找。
- 维护一个哈希表,key 存数值,value 存该数值对应的下标
- 遍历到当前元素
nums[i]时,计算需要的差值diff = target - nums[i] - 去哈希表里查这个差值是否存在:
- 存在:说明之前已经遇到过这个差值,两个数刚好配对,直接返回两个下标
- 不存在:把当前数值和下标存入哈希表,继续往后遍历
全程只需要一次遍历,每次查找和插入都是 O (1),整体时间复杂度降到 O (n)。
4. 关键细节:先查再存,避免自匹配
这里有一个非常重要的顺序:先判断差值是否存在,再把当前元素存入哈希表。 如果反过来先存再查,当前元素会被自己匹配到 ------ 比如 target=4,当前元素是 2,刚存进去就查到了自己,返回了两个相同的下标,不符合题目要求。
先查再存的话,哈希表里永远只有「当前元素之前的所有元素」,绝对不会出现自己和自己配对的情况,逻辑天然严谨。
三、两种解法完整代码
解法一:暴力枚举法(入门理解)
java
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
// 双重循环枚举所有数对
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
res[0] = i;
res[1] = j;
return res; // 找到直接返回
}
}
}
return res;
}
}
- 时间复杂度:O (n²)
- 空间复杂度:O (1)
解法二:哈希表一次遍历(面试最优解)
java
import java.util.HashMap;
import java.util.Map;
class Solution {
public int[] twoSum(int[] nums, int target) {
// 边界防御
if (nums == null || nums.length < 2) {
return new int[]{-1, -1};
}
Map<Integer, Integer> map = new HashMap<>();
// 一次遍历,边查边存
for (int i = 0; i < nums.length; i++) {
int diff = target - nums[i];
// 检查差值是否在之前出现过
if (map.containsKey(diff)) {
return new int[]{map.get(diff), i};
}
// 没找到就存入当前元素
map.put(nums[i], i);
}
return new int[]{-1, -1};
}
}
- 时间复杂度:O (n)
- 空间复杂度:O (n)
四、逐行代码深度拆解(哈希表版)
1. 边界防御
if (nums == null || nums.length < 2) {
return new int[]{-1, -1};
}
nums == null:防止空指针异常,保证数组对象存在nums.length < 2:数组不足两个元素,不可能组成数对,直接返回无效下标- 短路或顺序不能反,先判引用再判属性
2. 哈希表初始化
Map<Integer, Integer> map = new HashMap<>();
哈希表的键值设计:
- key:数组元素的数值,用于快速查找
- value:该数值对应的数组下标,用于返回结果
3. 单次遍历核心逻辑
int diff = target - nums[i];
if (map.containsKey(diff)) {
return new int[]{map.get(diff), i};
}
map.put(nums[i], i);
核心三步:
- 算差值:当前元素需要配对的另一个数是多少
- 查历史:去哈希表里找这个差值有没有出现过
- 存当前:没找到就把自己存进去,供后面的元素匹配
严格遵循「先查后存」的顺序,保证不会自匹配。
五、新手必踩坑清单
坑 1:数组只声明不初始化
- 错误写法:
int[] res; res[0] = -1; - 后果:空指针异常,只声明引用没创建数组对象
- 修正:必须用
new int[2]初始化数组
坑 2:找到答案不立即返回
- 后果:做大量无用遍历,性能浪费;多解场景下可能覆盖正确结果
- 原则:算法题找到答案立刻返回,避免多余运算
坑 3:先存后查,顺序颠倒
- 后果:元素自己匹配自己,返回相同下标
- 原则:永远先查差值,再存入当前元素
坑 4:内层循环从 0 开始
- 错误写法:
for(int j = 0; ...) - 后果:同一对数判断两次,且可能出现 i=j 的自匹配
- 原则:暴力法内层循环必须从
i+1开始
坑 5:哈希表键值搞反
- 错误:key 存下标,value 存数值
- 后果:无法通过数值快速查找,失去哈希表的意义
- 原则:用「要查找的内容」当 key,这里要按数值查找,所以数值做 key
六、面试相关
口述思路(直接背)
这道题我用哈希表来实现,采用空间换时间的思路。
我遍历数组,对于每个元素,计算它与目标值的差值,然后去哈希表中查找这个差值是否存在。如果存在,说明找到了两个数,直接返回两个下标;如果不存在,就把当前元素和它的下标存入哈希表。全程只需要一次遍历,查找操作是 O (1),整体时间复杂度 O (n),空间复杂度 O (n)。
这里要注意先查找再存入,避免同一个元素被使用两次。
高频追问
- 为什么不用排序 + 双指针? 因为题目要求返回原始下标,排序会打乱元素的原始位置。如果要保留下标需要额外存储,实现起来比哈希表更复杂,效率也没有优势。如果题目只需要判断是否存在、不需要返回下标,双指针是可行的。
- 如果数组中有重复元素怎么办? 不影响。题目保证只有唯一解,先出现的元素会先存入哈希表,后出现的元素匹配到就直接返回,不会出错。
- 暴力法和哈希法怎么选? 数组规模小时暴力法也能跑,但工程和面试中优先选哈希表,时间复杂度更优。如果严格限制空间复杂度,再考虑暴力法。
- 这道题的延伸题有哪些?
- 三数之和、四数之和:排序 + 双指针
- 两数之和 II(有序数组):双指针
- 两数之和 III(数据结构设计):哈希表设计
七、复习速记口诀
两数之和不用蛮,哈希查表省时间; 先算差值查历史,再存当前防自连; 暴力双重循环慢,空间换效是关键。
总结
两数之和作为算法入门第一题,价值不在于代码本身,而在于它引入了「空间换时间」的核心算法思想,以及哈希表在查找类问题中的强大作用。 从暴力双重循环到哈希表一次遍历,是非常经典的算法优化路径 ------ 找到耗时的瓶颈操作,用合适的数据结构把它的时间复杂度降下来,这也是解决绝大多数算法题的通用思考方式。 掌握这个思路后,后续的三数之和、四数之和等同类型题目,都可以快速迁移解法。