两数之和 图解题解
暴力两层循环慢得要命,但换个思路——每拿到一个数先查有没有现成的搭档,就能一次扫完。
就像你在人群里找一对号码牌之和等于目标数:每拿到一张牌,先在手边的「已见牌堆」里翻一翻,看有没有能配对的;没有的话再把这张牌压进堆里,继续往下走。重点是:先翻旧牌、再压新牌——不是先把所有牌都压进去再统一翻查。哈希表让每次翻查和压牌都是 O(1),整堆牌扫一遍就出答案。
这道题到底在问什么
- 输入
- nums=[2,7,11,15], target=9
- 输出
- [0, 1] (2 + 7 = 9)
先想最直接的笨办法
回看整条路径:指针从左到右只走一遍,每个数查一次表、(没配上才)存一次。这就是 O(n),比一上来嵌套两层循环挨个试的 O(n²) 快得多。(动画第 23 步)
最优解:为什么这么做
一句话答案:LeetCode 1 两数之和的最优解是哈希表一次遍历:从左到右扫数组,对每个数 x 先查 target - x 是否已经在表里,查到就立刻返回两个下标,没查到再把 x 存进表。每个数只做一次 O(1) 的查询和一次写入,整体时间 O(n)、空间 O(n),比两层循环的 O(n²) 暴力快一个数量级。
这道题真正在问什么
题目给一个数组 nums 和一个目标值 target,要找出两个不同下标 i、j,使 nums[i] + nums[j] 恰好等于 target,返回这两个下标。题目保证恰好存在一组答案,且同一个元素不能用两次。注意要返回的是下标而不是数值本身——这个细节决定了后面「排序再找」的路子并不划算,因为排序会打乱原始下标。
为什么暴力两层循环会慢
最直觉的做法是两层循环:外层固定一个数,内层把它后面的每个数都试一遍,看相加是否等于 target。数组有 n 个数时,最坏要试大约 n²/2 对组合,时间复杂度 O(n²)。慢的根源在于「找搭档」这一步是线性扫描:每固定一个数,都要把剩下的数从头到尾翻一遍。
换个角度想,当我们站在某个数 x 面前时,其实完全知道要找的搭档是谁——就是 target - x 这个确定的值。问题只剩下一件事:怎么用比线性扫描更快的方式,判断这个值有没有出现过。
为什么两数之和要用哈希表
「判断某个值出现过没有、出现在哪」正是哈希表(散列表)最擅长的事:插入和查询平均都是 O(1)。于是解法自然成形——建一张表记录「已经见过的值 → 它的下标」,从左到右扫数组,每到一个数 x 就先问表:target - x 在不在?在,说明搭档早就出现过,直接返回它记录的下标和当前下标;不在,就把 x 和它的下标存进表,继续往右走。
这样原来内层循环的整段线性扫描,被压缩成了一次 O(1) 的查表。n 个数每个只查一次、至多存一次,总时间就从 O(n²) 降到 O(n),代价是一张最多存 n 个数的表,空间 O(n)——用空间换时间的经典交易。
为什么必须先查再存,顺序不能反
循环体内「查 target - x」在前、「存 x」在后,这个顺序是正确性的关键。因为查的对象永远是「当前数左边已经出现过的数」,当前数还没入表,天然不可能查到自己——即使 target 恰好等于 2x,也不会误用同一个元素两次。
反过来,如果先把所有数一股脑存进表再来查,当 target - x 恰好等于 x 本身、而 x 只出现一次时,查到的其实是自己,会返回同一个下标两次,答案就错了。先查后存还有一个附带好处:找到答案的那一刻当前数根本不用入表,循环直接结束。
复杂度怎么算,边界在哪里
时间 O(n):数组扫一遍,每个数做一次哈希查询、至多一次哈希写入,均摊都是 O(1)。空间 O(n):最坏情况答案在最后一对,前面的数几乎全部进表。
两个容易踩的坑:一是哈希表的键值方向——键存「数值」、值存「下标」,因为我们是拿值反查下标,存反了就查不到;二是如果数组本身有序,更省内存的做法是左右对撞双指针,O(n) 时间、O(1) 空间(LeetCode 167 就是这个变体),但本题数组无序且要原始下标,哈希表一次遍历是最直接的答案。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住这条「查另一半 → 不在就存自己」,下面每一帧都在套它。
- 4一张空哈希表 + 一个从左往右走的指针。它每走到一个数,就做两件事:查另一半、再决定存不存自己。
- 5走到下标 0 的数 11,要配成 10 还差 -1。去哈希表里翻一遍,-1 不在表里,说明前面没出现过能跟它配对的数。
- 6既然 11 暂时配不上,就把它记进表里:键是值 11、值是下标 0。这样以后任何数只要差的正好是 11,一查就能立刻找到它在下标 0。
- 7走到下标 1 的数 15,要配成 10 还差 -5。去哈希表里翻一遍,-5 不在表里,说明前面没出现过能跟它配对的数。
- 8既然 15 暂时配不上,就把它记进表里:键是值 15、值是下标 1。这样以后任何数只要差的正好是 15,一查就能立刻找到它在下标 1。
- 9走到下标 2 的数 8,要配成 10 还差 2。去哈希表里翻一遍,2 不在表里,说明前面没出现过能跟它配对的数。
- 10既然 8 暂时配不上,就把它记进表里:键是值 8、值是下标 2。这样以后任何数只要差的正好是 8,一查就能立刻找到它在下标 2。
- 11走到下标 3 的数 1,要配成 10 还差 9。去哈希表里翻一遍,9 不在表里,说明前面没出现过能跟它配对的数。
- 12既然 1 暂时配不上,就把它记进表里:键是值 1、值是下标 3。这样以后任何数只要差的正好是 1,一查就能立刻找到它在下标 3。
- 13走到下标 4 的数 12,要配成 10 还差 -2。去哈希表里翻一遍,-2 不在表里,说明前面没出现过能跟它配对的数。
- 14既然 12 暂时配不上,就把它记进表里:键是值 12、值是下标 4。这样以后任何数只要差的正好是 12,一查就能立刻找到它在下标 4。
- 15走到下标 5 的数 5,要配成 10 还差 5。去哈希表里翻一遍,5 不在表里,说明前面没出现过能跟它配对的数。
- 16既然 5 暂时配不上,就把它记进表里:键是值 5、值是下标 5。这样以后任何数只要差的正好是 5,一查就能立刻找到它在下标 5。
- 17走到下标 6 的数 13,要配成 10 还差 -3。去哈希表里翻一遍,-3 不在表里,说明前面没出现过能跟它配对的数。
- 18既然 13 暂时配不上,就把它记进表里:键是值 13、值是下标 6。这样以后任何数只要差的正好是 13,一查就能立刻找到它在下标 6。
- 19走到下标 7 的数 3,要配成 10 还差 7。去哈希表里翻一遍,7 不在表里,说明前面没出现过能跟它配对的数。
- 20既然 3 暂时配不上,就把它记进表里:键是值 3、值是下标 7。这样以后任何数只要差的正好是 3,一查就能立刻找到它在下标 7。
- 21走到下标 8 的数 2,还差 8。这次去表里一查,8 在!它早在下标 2 时就被存进来了。两半凑齐,配对成功。
- 22下标 2 的 8 和下标 8 的 2 绿色高亮,它们正好加成 10。注意:第二个数(当前数)压根没存进表,一进来就被配走了。
- 23回看整条路径:指针从左到右只走一遍,每个数查一次表、(没配上才)存一次。这就是 O(n),比一上来嵌套两层循环挨个试的 O(n²) 快得多。
⚠️ 容易写错的地方
✗ 错:先把所有数都存进哈希再找
✓ 对:边遍历边查,查在前、存在后
若先存完,自己会查到自己(need=x 且 x 只出现一次时会误配同一下标)
✗ 错:查到了用同一个元素两次
✓ 对:查的是「之前已存的」,当前数还没存,天然避开自身
本题规定同一元素不能用两次
✗ 错:存键存成下标
✓ 对:哈希键存「值」、值存「下标」
我们要按「值」反查下标,键值别放反
完整代码(Python / C++ / Java)
Python
def twoSum(nums, target):
seen = {} # 值 -> 下标
for i, x in enumerate(nums):
need = target - x # 还差的另一半
if need in seen: # 另一半见过了
return [seen[need], i]
seen[x] = i # 没配上,存自己
return []C++
vector<int> twoSum(vector<int>& nums, int target){
unordered_map<int,int> seen; // 值 -> 下标
for(int i = 0; i < nums.size(); i++){
int need = target - nums[i];
auto it = seen.find(need);
if(it != seen.end()) return {it->second, i};
seen[nums[i]] = i; // 没配上,存自己
}
return {};
}Java
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> seen = new HashMap<>(); // 值 -> 下标
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i]; // 还差的另一半
if (seen.containsKey(need)) { // 另一半见过了
return new int[]{seen.get(need), i};
}
seen.put(nums[i], i); // 没配上,存自己
}
return new int[0];
}复杂度
时间
O(n)
每个数只查一次、存一次哈希,遍历一遍
空间
O(n)
最坏要把几乎所有数都存进哈希表
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 两数之和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如果数组是有序的,还会用哈希吗?+
有序时更优解是「对撞双指针」(左右往中间走),O(n) 时间但 O(1) 空间,比哈希省内存(见 LC167)。无序就用本题的哈希一遍。
如果要返回的是「两个数的值」而不是下标,做法变吗?+
不变,逻辑一样,只是返回 [need, x] 而非下标。哈希表照样记录见过的数即可。
为什么不直接排序后双指针?+
排序会打乱原下标,本题要返回原始下标,排完还得映射回去,反而更麻烦;无序场景哈希一遍最直接。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 两数之和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。