题目描述
思路解析
一句话答案: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 就是这个变体),但本题数组无序且要原始下标,哈希表一次遍历是最直接的答案。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这条「查另一半 → 不在就存自己」,下面每一帧都在套它。
一张空哈希表 + 一个从左往右走的指针。它每走到一个数,就做两件事:查另一半、再决定存不存自己。
走到下标 0 的数 11,要配成 10 还差 -1。去哈希表里翻一遍,-1 不在表里,说明前面没出现过能跟它配对的数。
既然 11 暂时配不上,就把它记进表里:键是值 11、值是下标 0。这样以后任何数只要差的正好是 11,一查就能立刻找到它在下标 0。
走到下标 1 的数 15,要配成 10 还差 -5。去哈希表里翻一遍,-5 不在表里,说明前面没出现过能跟它配对的数。
既然 15 暂时配不上,就把它记进表里:键是值 15、值是下标 1。这样以后任何数只要差的正好是 15,一查就能立刻找到它在下标 1。
走到下标 2 的数 8,要配成 10 还差 2。去哈希表里翻一遍,2 不在表里,说明前面没出现过能跟它配对的数。
既然 8 暂时配不上,就把它记进表里:键是值 8、值是下标 2。这样以后任何数只要差的正好是 8,一查就能立刻找到它在下标 2。
走到下标 3 的数 1,要配成 10 还差 9。去哈希表里翻一遍,9 不在表里,说明前面没出现过能跟它配对的数。
既然 1 暂时配不上,就把它记进表里:键是值 1、值是下标 3。这样以后任何数只要差的正好是 1,一查就能立刻找到它在下标 3。
走到下标 4 的数 12,要配成 10 还差 -2。去哈希表里翻一遍,-2 不在表里,说明前面没出现过能跟它配对的数。
既然 12 暂时配不上,就把它记进表里:键是值 12、值是下标 4。这样以后任何数只要差的正好是 12,一查就能立刻找到它在下标 4。
走到下标 5 的数 5,要配成 10 还差 5。去哈希表里翻一遍,5 不在表里,说明前面没出现过能跟它配对的数。
既然 5 暂时配不上,就把它记进表里:键是值 5、值是下标 5。这样以后任何数只要差的正好是 5,一查就能立刻找到它在下标 5。
走到下标 6 的数 13,要配成 10 还差 -3。去哈希表里翻一遍,-3 不在表里,说明前面没出现过能跟它配对的数。
既然 13 暂时配不上,就把它记进表里:键是值 13、值是下标 6。这样以后任何数只要差的正好是 13,一查就能立刻找到它在下标 6。
走到下标 7 的数 3,要配成 10 还差 7。去哈希表里翻一遍,7 不在表里,说明前面没出现过能跟它配对的数。
既然 3 暂时配不上,就把它记进表里:键是值 3、值是下标 7。这样以后任何数只要差的正好是 3,一查就能立刻找到它在下标 7。
走到下标 8 的数 2,还差 8。这次去表里一查,8 在!它早在下标 2 时就被存进来了。两半凑齐,配对成功。
下标 2 的 8 和下标 8 的 2 绿色高亮,它们正好加成 10。注意:第二个数(当前数)压根没存进表,一进来就被配走了。
回看整条路径:指针从左到右只走一遍,每个数查一次表、(没配上才)存一次。这就是 O(n),比一上来嵌套两层循环挨个试的 O(n²) 快得多。
边界先想清:哪怕有重复值,因为「先查后存」,第二个相同的数来时正好查到第一个。
三个高频追问:区分「无序用哈希、有序用双指针」,以及为什么不排序。
参考代码
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 []复杂度
- 时间:O(n),每个数只查一次、存一次哈希,遍历一遍
- 空间:O(n),最坏要把几乎所有数都存进哈希表
易错点
面试追问把动画讲成自己的话
追问如果数组是有序的,还会用哈希吗?
追问如果要返回的是「两个数的值」而不是下标,做法变吗?
追问为什么不直接排序后双指针?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
字母异位词分组
LeetCode 49 · 中等 · 沿着 数组 & 哈希 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题