
两数之和大概是所有刷题人最熟悉的陌生人。说它“熟悉”是因为它常年占据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]内层循环从i1开始找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]; }这里有个细节内层循环为什么从i1开始因为题目禁止使用同一个元素两次同时避免(i, j)和(j, i)这种重复统计。如果从0开始你会遇到i j的情况也就是同一个位置自己加自己那就有问题了。就算你加判断跳过i j也会白给很多无效计算。所以从i1开始是标准写法。2.2 时间复杂度分析双重循环的时间复杂度是 O(n²)空间复杂度是 O(1)。这个复杂度很多人会背但未必理解它的含义。比如数组长度是 10内层循环次数大概是 98...145 次如果长度是 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) { MapInteger, 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。遍历到第一个3complement是3没有存3 - 0遍历到第二个3complement是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之前。没有使用i1作为内层循环起点暴力解时产生重复对或者自己跟自己配对。定义哈希表时写错了泛型比如MapInteger, Integer写成了MapInteger, int[]编译直接报错。循环里没有处理数组长度为 0 或 1 的边界直接访问nums[1]导致数组越界。虽然原题可能不会给这种输入但健壮的解法还是应该提前判断。排查时你可以打印map的内容或者用几个小例子手动走一遍nums[1,2,3,4], target3、nums[3,3], target6、nums[-1,-2,-3,-4], target-7。这三组用例分别覆盖常规情况、重复值、负数情况能快速暴露90%的问题。6.3 面试中如何讲解思路面试时不要上来就写代码。更好的节奏是先确认需求比如“数组有序吗有没有重复是不是保证有解返回值顺序有没有要求”这些问题不仅让你显得严谨还可能帮你避开题目陷阱。然后给出暴力解分析复杂度再提出优化。回答哈希表思路时可以用一句话概括“我遍历数组对于当前元素我只关心它之前出现过的数中有没有它的补数。如果有直接返回如果没有就把当前元素存进哈希表。因为每个元素最多被扫描一次所以时间复杂度是O(n)。” 这句话含金量很高面试官一听就知道你是真懂。如果面试官问“能不能不用额外空间”你可以顺带提一下双指针思路但那要求数组有序。在原题无序的情况下排序本身要O(n log n)反而不如哈希表。你能主动说出这种权衡会比只会背答案强得多。最后再说点我自己的体会我自己的体会是两数之和这道题最妙的地方就在那一个“补数”视角——把加法问题变成了减法问题。你从nums[i] ? target改成? target - nums[i]整个解法豁然开朗。这个思维转换能力比任何代码模板都值钱。很多所谓难题不过是把这种转换藏得更深了而已。还有一个小技巧如果你在面试时写完了哈希表解法可以顺手提一下“如果数组非常大还可以想想分布式或流式处理”这种延伸性的话会让人眼前一亮。当然别硬吹知道多少说多少。最后想说的是刷题不是比数量比的是每一道题有没有建立“连接”。两数之和连接了哈希表、连接了配对问题、连接了一堆热门题把这道题嚼碎了你后面的刷题路会顺很多。按照先跑通暴力解、再理解优化思路、最后总结成笔记的节奏来这道题一定能变成你的送分题。