两数之和大概是所有刷题人最熟悉的陌生人。说它“熟悉”,是因为它常年占据LeetCode题库的头号位置,几乎每个人入门时都会碰到;说它“陌生”,是因为真正能把这道题讲透、做明白、想清楚的人并没有那么多。我见过太多人一上来就背哈希表写法,结果被问到“为什么用哈希表”“如果数组里有重复元素怎么办”就答不上来。这篇文章就以LeetCode第1题两数之和为切口,帮大家把题目背后的算法思维、代码细节、面试讲解逻辑一次性捋清楚,顺便聊聊它在热门100题和后续刷题路线中的位置,适合刚开始刷题的新人,也适合准备面试但基础不牢的老选手。
1. 两数之和这道题到底在考什么
1.1 题目描述与核心需求
先看原题,虽然很多人已经背下来了,但我还是建议重新思考一遍:给定一个整数数组nums和一个整数目标值target,要求在数组中找出和为目标值的两个整数,并返回它们的数组下标。注意几个关键词:整数数组、目标值、两个整数、返回下标。题目没有说数组是否有序,没有说是否有重复元素,没有说是否保证有解,但按LeetCode 1的标准版本,默认满足“恰好一个答案”且“不能使用同一个元素两次”。这个默认条件极其重要,直接影响解法写不写得出来,我后面会详细说。
这道题表面上是个查找问题,本质上是“配对”问题。你把数组想象成一个聚会现场的人头列表,每个人身上贴着一个数字,现在要找出两个人的数字加起来正好等于某个定值。最直觉的做法当然是一个一个试,但算法课教我们,直觉要先翻译成复杂度,复杂度再反推数据结构选型。这才是题目真正想考的。
1.2 这道题的经典地位
在LeetCode所有题目里,第1题的特殊性在于它是所有“数组 + 哈希表”组合的启蒙题。它的难度标记为Easy,但面试中出现频率极高,尤其是在初级岗位的筛选中,几乎成了“手速题”——不是看你写不写得出来,而是看你能不能一分钟内写出最优解并讲清楚原理。
另外,它也是很多“套路”的源头:两数之和的思路扩展出去就是三数之和、四数之和、两数之和输入有序数组、两数之和BST版等。在“LeetCode热门100题”里,这类基于两数之和思想的题目少说也有十几道。所以我一直认为,刷题不能只追求AC,要把每一道经典题吃透,让一道题变成一类题的模板,这样刷一百道抵别人三百道。
2. 暴力解法:先跑通再优化
2.1 双重循环的实现
别小看暴力解法,很多人第一次写两数之和就是双重循环。它最直白,也最容易验证思路。核心写法是:外层循环固定第一个数字nums[i],内层循环从i+1开始找nums[j],判断nums[i] + nums[j] == target,如果相等就返回{i, j}。因为题目保证有且仅有一个解,所以找到后直接返回即可,不需要考虑找不到的情况。
代码大概长这样:
public int[] twoSum(int[] nums, int target) { for (int i = 0; i < nums.length; i++) { for (int j = i + 1; j < nums.length; j++) { if (nums[i] + nums[j] == target) { return new int[] {i, j}; } } } return new int[0]; }这里有个细节:内层循环为什么从i+1开始?因为题目禁止使用同一个元素两次,同时避免(i, j)和(j, i)这种重复统计。如果从0开始,你会遇到i == j的情况,也就是同一个位置自己加自己,那就有问题了。就算你加判断跳过i == j,也会白给很多无效计算。所以从i+1开始是标准写法。
2.2 时间复杂度分析
双重循环的时间复杂度是 O(n²),空间复杂度是 O(1)。这个复杂度很多人会背,但未必理解它的含义。比如数组长度是 10,内层循环次数大概是 9+8+...+1=45 次;如果长度是 1000,就接近 50 万次。实际面试时,如果面试官问你“数据量是多少”,暴力解法能不能扛住,你要能快速估算:1万条数据就是约 5000 万次比较,在普通机器上大概零点几秒到几秒级别;10万条直接就是亿级别,基本没法跑。
那为什么还要讲暴力解?因为它是推导最优解的起点。你自己写一遍暴力解,能直观感受“重复计算”发生在哪里:对于每一个i,你都会把后面所有的数都扫一遍,而前面的扫描结果完全没有被保存下来。这正好引出了哈希表的核心优势。
2.3 暴力解的适用场景
虽然 O(n²) 一般不优秀,但有些场景下它反而是合理的。比如数组非常短,只有几个元素,或者你只打算临时用一下、不想引入额外数据结构,又或者你要在一个不支持哈希表的极简环境里写逻辑,那暴力解就是最稳的选择。
另外,面试时如果你第一时间没想出最优解,完全可以先说暴力解,然后分析复杂度,再过渡到优化方案。这比憋着不说话强一百倍。面试官更看重的是你的思维过程,而不是一上来就背答案。
3. 哈希表解法:用空间换时间
3.1 核心思路:补数思想
优化的关键是把“找另一个数”的过程从遍历变成查询。暴力解慢的内因是每次都要扫一遍剩余数组才能知道“有没有我需要的数”。如果我们能把每个数出现的位置记下来,那就只需要看一眼备忘录就立刻知道答案。这个备忘录就是哈希表。
具体来说,我走到第 i 个位置时,需要找的目标是target - nums[i],这个值通常被称为“补数”。如果哈希表里已经存过这个补数,那就直接返回;如果还没有,就把当前数字和它的下标存进去。整个过程只需要一次遍历,时间复杂度降到 O(n),代价是额外 O(n) 的哈希表空间。
你可以把这个过程类比成玩配对游戏:你手里有一张号码牌,记下自己号码后,去查看公告板上有没有能跟你补齐目标值的另一张号码牌。有就直接配对,没有就把自己的号码牌贴到公告板上。公告板就是哈希表。
3.2 一次遍历的写法
常见的写法是先建哈希表,然后遍历数组,边查边存。以Java为例:
public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int complement = target - nums[i]; if (map.containsKey(complement)) { return new int[] {map.get(complement), i}; } map.put(nums[i], i); } return new int[0]; }这里有一个非常关键的设计:为什么不是先循环把所有数字都放进哈希表,再循环查找一遍?那样其实也行,但对重复元素会出问题。比如nums = [3, 3],target = 6,如果先全部存入,那么map.get(3)会被后一个3覆盖,再查询时你要手动判断下标是否相等,代码更啰嗦。而一次遍历的方式,因为每次先查再存,天然避开了“当前元素自己和自己配对”的问题:查询发生在put之前,所以哈希表里存的一定是当前元素之前出现过的数。
这种“先查后存”的顺序,是这道题最容易踩的坑。很多人看过答案后自己写,手一滑写成先put再containsKey,导致同一个元素被算了两次,在target刚好是某个元素两倍时返回错下标。
3.3 为什么能保证找到正确下标
哈希表存的是<数值, 下标>的映射,所以一旦命中complement,我们可以 O(1) 取出它第一次出现(或者说之前最后一次出现)的下标。由于题目保证只有唯一答案,所以只要找到了就是正确的。
还有一点值得注意:这个解法不需要关心数组是否有序,也不需要关心正负数,因为哈希表只看值是否存在,不看相对顺序。这也是它比双指针更通用的原因。双指针要求数组有序,而两数之和原题不保证有序,所以哈希表才是第一选择。
4. 边界条件与易错点
4.1 数组里有重复值怎么办
很多人一看到重复值就慌了。其实只要用一次遍历的哈希表,重复值完全不是问题。举个例子:nums = [3, 2, 4],target = 6。遍历到第一个3时,complement是3,哈希表为空,所以把3 -> 0存进去;遍历到2时,complement是4,不存在,存2 -> 1;遍历到4时,complement是2,哈希表里有,返回[1, 2],正确。
再看nums = [3, 3],target = 6。遍历到第一个3,complement是3,没有,存3 -> 0;遍历到第二个3,complement是3,哈希表里有,返回[0, 1],正确。整个过程不需要额外判断map.get(complement) != i,因为查的时候还没把当前下标存进去。
但如果你用了“先全部存入再查询”的写法,就必须要处理重复值覆盖问题。所以我一直建议面试时直接写一次遍历版本,逻辑更顺,容错更高。
4.2 找不到答案的情况
原题明确说“假定只有一个有效答案”,所以可以不处理无解分支。但实际面试时,面试官很可能会追问:“如果没有解怎么办?”这时候你要知道,返回空数组是一种约定做法,比如return new int[0],而不是返回null。返回null会导致调用方直接空指针,在真实工程里是很糟糕的实践。
如果面试官进一步要求返回“任意一对”或“所有对”,那就又不一样了。所有对的话,哈希表里可能需要存一个值对应的多个下标,用List作为 value,然后还要考虑去重。不过那就超出 Easy 题范围了,面试中很少要求,但你要能说出思路,会显得思考很深。
4.3 下标顺序与题目要求
LeetCode原题要求返回的是两个下标,顺序无所谓,因为最终的校验只看两个位置的值加起来是否等于target。但有些变种题目(比如返回有序数组的两数之和)可能要求按某种顺序返回,这时就要仔细读题。
另一个容易错的点:返回的是下标,不是数值。很多新手第一次写,直接把nums[i]和nums[j]返回了,结果当然是错。还有一个细节是,哈希表的 key 存的是数组元素值,value 存的是下标,千万别写反,写反了map.get(nums[i])时取出来的是值,不是位置,整个程序就全乱了。
5. 从一题到一类:两数之和的变形与应用
5.1 两数之和在现实业务中的映射
很多人觉得这道题太“算法竞赛”,好像工作里用不到。其实不是的,你把“数组”换成“订单列表”,“target”换成“某个目标金额”,两数之和就是最常见的电商凑单问题:找出两个订单金额之和等于某个优惠门槛的订单。再比如风控场景中,找出两台设备同时出现在同一用户登录日志中的可疑组合,本质也是一类两数之和的配对问题。
我曾在做活动系统时遇到过这样一个需求:已知一批商品ID和价格,运营希望找出价格合计刚好等于某档位优惠券门槛的两件商品。数据量大概几千条,直接双重循环也就百万级别,其实可以接受。但如果数据量到几十万,就必须用哈希表了。所以这道题不是纯粹的智力游戏,它解决的是实实在在的配对查找问题,只不过给你套了个数组的外壳。
5.2 高频变体:三数之和、四数之和
两数之和的推导逻辑可以自然延伸。三数之和的本质是先固定一个数,剩下的问题就变成“两数之和”,只不过此时不再是返回下标,而是返回具体的数值组合,且要去重。四数之和就是固定两个数,再解决剩下两数之和。这就是所谓“降维思想”。
在LeetCode热门100题中,三数之和(第15题)、四数之和(第18题)都和这道题强相关。如果你两数之和理解透了,那三数之和的排序+双指针解法你会学得很快;如果两数之和只是背代码,后面遇到三数之和就会觉得特别绕,因为你需要处理去重、跳过重复值、指针移动等更多细节。
5.3 与热门100题的关系
“LeetCode热门100题”是一个经典题库合集,其中数组与哈希表类占比相当高。两数之和作为开篇题,其实在给你建立两个习惯:一是“遇见查找,想哈希表”,二是“分析复杂度再动手”。这两个习惯负责解决大量中等题。
比如热门100题里的“字母异位词分组”“最长连续序列”“和为K的子数组”等,全都在用类似的两数之和思想,只是容器从两个数变成多个数、从子串变成子数组。所以我的建议是,刷题不要跳着刷,先把两数之和的哈希表写法焊死在脑子里,后面遇到相关题时你会回来感谢它。
6. 刷题经验与进阶建议
6.1 刷题时怎么记笔记
我见过太多人刷题就是“AC完就忘”,过两周再看到还是不会。两数之和这种题尤其典型,因为你可能花十分钟看懂了,但没记下思考过程,等于没刷。分享一下我的习惯:每道题用一个固定模板记笔记,包含题目编号、最优解法、复杂度、易错点、和哪些题目相关。
以两数之和为例,笔记我会写:
题1 两数之和 核心:补数 + 哈希表,一次遍历 复杂度:时间O(n),空间O(n) 易错:先查再存;返回下标不是值;重复元素无需额外判断 关联:三数之和、两数之和II、和为K的子数组别小看这几行字,一个月后复习时,你只需要30秒就能唤醒完整记忆。笔记的价值不在于写得漂亮,而在于把“当时怎么想的”压缩下来。
6.2 常见误区排查
如果你运行代码报错,多半是下面几个原因:
- 返回的是元素值而不是下标。检查你的
return new int[] {...}括号里传的是map.get(complement)和i,而不是complement和nums[i]。 - 先
put后查,导致同一个元素被用两次。解决方法是把containsKey放在put之前。 - 没有使用
i+1作为内层循环起点(暴力解时),产生重复对或者自己跟自己配对。 - 定义哈希表时写错了泛型,比如
Map<Integer, Integer>写成了Map<Integer, int[]>,编译直接报错。 - 循环里没有处理数组长度为 0 或 1 的边界,直接访问
nums[1]导致数组越界。虽然原题可能不会给这种输入,但健壮的解法还是应该提前判断。
排查时你可以打印map的内容,或者用几个小例子手动走一遍:nums=[1,2,3,4], target=3、nums=[3,3], target=6、nums=[-1,-2,-3,-4], target=-7。这三组用例分别覆盖常规情况、重复值、负数情况,能快速暴露90%的问题。
6.3 面试中如何讲解思路
面试时不要上来就写代码。更好的节奏是:先确认需求,比如“数组有序吗?有没有重复?是不是保证有解?返回值顺序有没有要求?”这些问题不仅让你显得严谨,还可能帮你避开题目陷阱。然后给出暴力解,分析复杂度,再提出优化。
回答哈希表思路时,可以用一句话概括:“我遍历数组,对于当前元素,我只关心它之前出现过的数中有没有它的补数。如果有,直接返回;如果没有,就把当前元素存进哈希表。因为每个元素最多被扫描一次,所以时间复杂度是O(n)。” 这句话含金量很高,面试官一听就知道你是真懂。
如果面试官问“能不能不用额外空间”,你可以顺带提一下双指针思路,但那要求数组有序。在原题无序的情况下,排序本身要O(n log n),反而不如哈希表。你能主动说出这种权衡,会比只会背答案强得多。
最后再说点我自己的体会
我自己的体会是,两数之和这道题最妙的地方就在那一个“补数”视角——把加法问题变成了减法问题。你从nums[i] + ? = target改成? = target - nums[i],整个解法豁然开朗。这个思维转换能力,比任何代码模板都值钱。很多所谓难题,不过是把这种转换藏得更深了而已。
还有一个小技巧,如果你在面试时写完了哈希表解法,可以顺手提一下“如果数组非常大,还可以想想分布式或流式处理”,这种延伸性的话会让人眼前一亮。当然,别硬吹,知道多少说多少。
最后想说的是,刷题不是比数量,比的是每一道题有没有建立“连接”。两数之和连接了哈希表、连接了配对问题、连接了一堆热门题,把这道题嚼碎了,你后面的刷题路会顺很多。按照先跑通暴力解、再理解优化思路、最后总结成笔记的节奏来,这道题一定能变成你的送分题。