两数之和:从暴力枚举到哈希表,掌握算法优化的核心思维

发布时间:2026/9/13 19:41:27
两数之和:从暴力枚举到哈希表,掌握算法优化的核心思维 打开 LeetCode 准备开始刷题几乎所有人的第一站都是这道两数之和。说实话这道题在热门 100 题里长期霸榜不是因为它难而是因为它太经典了题目短、思路多、优化路径清晰适合拿来建立刷题的基本功。无论你是刚接触算法的新手还是准备面试想在短时间内找回手感的老兵把这道题吃透都会比盲目刷几十道题更有价值。很多人在 LeetCode 上点开这道题看到题目简单直接暴力双层循环一跑通过之后就算完事了。但这样其实错过了这道题 80% 的信息量。两数之和背后是一个完整的算法思考流程从暴力枚举到空间换时间从数据结构的选择到边界条件的处理。我自己后来在刷 LeetCode 周赛和其他经典题目时反复用到这套思维路径可以说这道题的后劲比表面看起来大得多。1. 刷题前先搞懂两数之和到底在考什么1.1 题目拆解输入输出到底是什么先把题目条件重新梳理一遍。给定一个整数数组nums和一个整数目标值target要求你在数组中找出和为目标值的那两个整数并返回它们的数组下标。注意几个容易被忽略的细节第一每种输入只会对应一个答案也就是说答案唯一第二同一个元素不能使用两次第三你可以按任意顺序返回答案这对做题顺序没有强制要求。举个最基础的例子nums [2, 7, 11, 15]target 9那么返回[0, 1]因为2 7 9。如果nums [3, 2, 4]target 6那么返回[1, 2]而不是[0, 0]因为下标0对应的元素 3 只有一份不能自己加自己。这个不能使用同一个元素的限制直接决定了很多解法里去重和判断下标的写法也是很多人第一次写错的地方。从考点来看这道题不仅仅是考你会不会写代码更是在考你对数组操作、查找效率、哈希表应用的综合理解。面试官常常通过这道题观察你的思维过程拿到问题后是先写暴力解再优化还是直接想到最优解写完代码后有没有主动分析时间复杂度和空间复杂度能不能举出特殊用例来测试自己的代码。这些都是比AC更重要的事情。1.2 为什么这是 LeetCode 热门 100 题第一题LeetCode 热门 100 题是很多人的刷题起点而两数之和排在第一的位置既和题目编号是 1 有关也和它的教学价值有关。从用户反馈来看这道题覆盖了最基础的数组遍历、哈希表碰撞处理、复杂度分析等核心概念同时难度评级为 Easy适合用来建立信心。更重要的是这道题是许多高阶题目的最小原型。比如三数之和、四数之和本质上都是在两数之和的基础上做扩展再比如两数之和 II输入有序数组、两数之和 III设计数据结构这些变体也都是从这道题的知识点延伸出来的。LeetCode 周赛中也经常出现类似的思路只是换了一层包装核心仍然是快速找到两个数满足某个条件。所以我的建议是不要因为题目简单就跳过去。花点时间把这道题所有可能的解法都写一遍包括暴力解、两遍哈希表、一遍哈希表甚至有序数组下的双指针解法。这个过程会帮助你建立起同一个问题可以有多种解法的意识而这一点在真正的面试和实际工程中都非常重要。2. 暴力解不是不能写但得知道它为什么慢2.1 双层循环的思路与时间复杂度分析看到两数之和的第一反应很多人的直觉就是两层循环外层遍历每个元素内层遍历它后面的所有元素判断两个数加起来是否等于 target。这个思路没有任何问题代码也很简单def twoSum_brute(nums, target): n len(nums) for i in range(n): for j in range(i 1, n): if nums[i] nums[j] target: return [i, j] return []这段代码为什么可行因为答案唯一只要找到一组就立刻返回不会出现多组答案干扰的问题。内层循环从j i 1开始也天然避免了同一个元素被使用两次的情况。时间复杂度是 O(n²)外层循环跑 n 次内层循环平均跑 n/2 次总的比较次数大约是n * (n-1) / 2。当 n 很小的时候比如 n 10这个开销无所谓但当 n 是 10 万甚至 100 万时O(n²) 就完全跑不动了。我见过有人在 LeetCode 上提交暴力解后超时原因就是测试数据里有一个长度很大的数组比如 LeetCode 的某些测试用例会有几万个元素两层循环需要比较上亿次即使语言运行再快也会超时。空间复杂度是 O(1)因为除了输入数组之外我们没有额外使用任何数据结构。这也是暴力解唯一的优势省内存。但问题在于实际刷题和面试中时间复杂度往往比空间复杂度更关键尤其是在空间比较充裕的现代环境下。2.2 从暴力解到哈希表优化动机如何产生暴力解慢的根本原因是在内层循环查找另一个数时使用的是线性扫描。每一次都要从头到尾逐个比较没有任何记忆能力。假如我们能记住已经遍历过的数并且能在 O(1) 时间内判断target - 当前数是否存在于之前遍历过的数中整个算法就能从 O(n²) 降到 O(n)。怎么实现这种记忆 快速查询答案就是哈希表。哈希表在 Python 中是字典在 Java 中是 HashMap的核心优势在于它能让查找操作的平均时间复杂度降为 O(1)。用一个生活化的类比暴力解就像你在一本没有目录的书里找某个关键词每次都要一页一页翻哈希表则像给每个关键词都建了一张索引卡你想找什么直接去对应位置取就好。这个思考过程是最有价值的不是直接跳到最后的最优解而是先认识到暴力解瓶颈在哪里再由瓶颈推导出需要什么样的数据结构来解决问题。这种发现瓶颈 - 设计解药的路径才是刷题时最应该训练的能力。两数之和恰恰是这条路径最标准、最简洁的一个样例所以它才会成为无数算法课程和面试准备材料里的第一道题。3. 哈希表解法核心细节与代码实现3.1 一遍哈希表的完整逻辑哈希表层常见的有两种写法两遍哈希表和一遍哈希表。两遍哈希表的思路是第一次遍历把所有元素的值和下标存进哈希表第二次遍历对每个元素去哈希表里查target - nums[i]注意要过滤掉下标相同的情况。一遍哈希表则更简洁在遍历数组的过程中边查边存。一遍哈希表的核心逻辑是这样的对于当前元素nums[i]我们想找的是target - nums[i]如果这个值已经在哈希表中说明我们在之前的遍历中遇到过它那么直接返回哈希表中存储的下标和当前下标。如果没找到就把nums[i]作为 keyi作为 value 存进哈希表然后继续往后遍历。为什么要边查边存而不是先把所有数据都存好因为这样做可以天然避开同一个元素使用两次的问题。考虑nums [3, 3]target 6如果先把两个 3 都存进哈希表key 重复需要额外处理冲突非常麻烦而一边遍历一边存的话处理第一个 3 时哈希表还是空的查不到6 - 3 3于是把(3, 0)存进去处理第二个 3 时查到哈希表里有 3直接返回[0, 1]逻辑非常干净。3.2 代码实现Python、Java、C 三种写法Python 的字典天然支持哈希表操作代码可以写得非常简单def twoSum(nums, target): seen {} for i, num in enumerate(nums): complement target - num if complement in seen: return [seen[complement], i] seen[num] i return []Java 中使用HashMap也是类似思路注意泛型写法和方法的定义import java.util.HashMap; class Solution { public int[] twoSum(int[] nums, int target) { HashMapInteger, 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[] {}; } }C 的unordered_map使用也基本一致需要留意头文件的引入和返回值类型#include vector #include unordered_map class Solution { public: std::vectorint twoSum(std::vectorint nums, int target) { std::unordered_mapint, int seen; for (int i 0; i nums.size(); i) { int complement target - nums[i]; if (seen.find(complement) ! seen.end()) { return {seen[complement], i}; } seen[nums[i]] i; } return {}; } };三种语言的底层逻辑完全一致区别只在于哈希表的 API 和语法细节。建议你至少熟练掌握其中一种语言把上面的写法练到能够闭着眼睛打出来因为两数之和是面试中的高频手写题几乎每个面试官都可能让你现场写。3.3 边界条件和细节陷阱哈希表解法虽然简单但有几个地方非常容易出问题。第一个陷阱是target - num可能溢出。在 C 和 Java 中如果num和target都是int类型target - num的结果在某些极端情况下可能会超出int范围。LeetCode 的原始测试数据一般不会触发但在实际工程中不能忽视。Python 的整数没有溢出问题所以不存在这个担忧。第二个陷阱是空数组和只含一个元素的数组。很多新手写完代码后直接用题目给的例子测试通过了就提交结果碰到nums []或nums [1]时直接返回空这时候代码里的循环不会执行最后返回[]或空数组逻辑上没问题。但有些面试官会要求你对这种边界情况给出明确的处理策略最好在函数开头加上判断。第三个陷阱是重复元素的处理顺序。一遍哈希表策略下如果数组中存在重复元素我们总是把后出现的下标覆盖到哈希表中已有的 key 上吗不对方便的方法是先查后存所以重复元素不会造成覆盖问题。但如果把顺序写成先存后查那么当nums [3, 3]时会得到错误结果因为第二次看到 3 时哈希表里的下标已经被更新成了 1查到的下标和当前下标相同会返回[1, 1]。这样虽然也能通过部分测试但实际上是错的不符合题目要求。所以务必记住先检查 complement 是否在表中再执行插入。第四个陷阱是返回值顺序。题目说可以按任意顺序返回所以返回[i, j]还是[j, i]都行。但如果你在面试中遇到面试官额外要求按下标从小到大返回那就要注意排序了不能直接用当前遍历顺序结束。最好在写代码之前和面试官确认清楚。4. 题目变形与面试追问这才是拉开差距的地方4.1 如果数组已经有序能不能用双指针很多人在刷完两数之和后会紧接着遇到 LeetCode 167两数之和 II - 输入有序数组这正是两数之和的第一种变形给定一个已经按升序排列的数组仍然找两个数之和等于 target但要求返回的下标从 1 开始计数。有序数组最大的变化是我们有了更高效的解法——双指针。一个指针指向数组头部另一个指针指向数组尾部计算当前两个指针指向元素的和。如果和大于 target说明需要减小和右指针左移如果和小于 target说明需要增大和左指针右移。直到找到答案或者两个指针相遇。双指针的时间复杂度是 O(n)空间复杂度是 O(1)在有序数组的场景下比哈希表方案更优因为连额外空间都不用了。这道变形题的意义在于提醒我们同一个问题在不同约束下最优解可能是完全不同的。如果一看到两数之和就条件反射上哈希表反而可能错过更优解法。刷题最忌讳的就是一招鲜吃遍天。4.2 如果要求返回所有不重复的组合怎么做另一个高频变体是三数之和。三数之和要求在数组中找到所有三元组使得a b c 0并且不允许出现重复的三元组。这个问题的解法非常经典先排序然后固定一个数再对剩余部分用双指针找两数之和。你会发现三数之和本质上就是排序 两数之和双指针版的组合。但难点在于去重哪怕数组中有重复元素最终结果里也不能出现重复三元组。这时候需要额外的去重逻辑比如在固定数时跳过相同的值在移动双指针时跳过重复的值。很多人在 LeetCode 周赛或面试中遇到三数之和都会卡在去重上但如果能把两数之和的变体逻辑吃透去重思路其实很好理解排序之后相同的值都聚在一起跳开就好。4.3 如果数据量极大、内存有限怎么办如果数组大到无法全部放入内存比如数据分布在多个文件或数据库中哈希表方案就不再适用因为哈希表本身也需要占用内存。这时候可以考虑外部排序 双指针的方式先在磁盘上对数据分块排序再通过外存归并的方式用双指针在有序的数据流上查找两数之和。整个过程相当于把内存限制转化为对排序和流式读取的考量。还有一类扩展是海量数据下的近似查找比如在一个很大的数据集中判断是否存在两个数之和接近 target而不是一定相等。这时可以用分布式哈希、布隆过滤器等工具。虽然这些已经超出 LeetCode 本身的范围但如果你在面试中能把思路延伸到这些方向会给面试官留下很好的印象。两数之和这道题其实是一个很好的由浅入深的面试互动话题。5. 刷题过程中的常见问题与排查技巧5.1 典型坑位速查表我自己刷这道题和带别人刷的时候发现大家最容易踩的坑高度集中。这里整理一个速查表方便你在卡住时快速对照症状可能原因解决方法返回[0,0]或[1,1]同一元素使用了两次检查是否是先存后查改为先查后存输出为空数组数组长度小于 2 或无解加边界判断返回空容器提交超时使用了 O(n²) 暴力解改用哈希表 O(n) 解法下标越界内层循环边界写错确保j从i1开始且小于数组长度返回值顺序与预期不符题目允许任意顺序但面试官可能有要求与面试官确认输出顺序对重复元素处理错误没有判断下标是否相同使用一遍哈希表的先查后存逻辑5.2 正确调试姿势边界测试与肉眼验证很多初学者提交代码前只跑题目自带的示例通过了就觉得万事大吉。这样做风险很高因为示例往往太简单覆盖不到边界情况。我的习惯是写完代码后至少额外测下面几组数据空数组nums []单元素数组nums [1]两元素数组nums [1, 2]如果target 3应该返回[0, 1]存在重复元素nums [3, 3]target 6答案不在数组中的情况nums [1, 2, 3]target 10负数参与nums [-1, 0, 3, 4]target 3这些测试数据不需要写代码直接在本地手动跑或者在 LeetCode 自测框里跑就行。重点是培养边界敏感度这在面试中非常加分。当面试官问你的代码有什么边界情况需要考虑时你能直接列举出来说明你确实理解了自己的代码。5.3 善用题解和社区资源但别急着看答案LeetCode 热题下有很多高质量题解中文区英文区都有我也常去翻。但我的建议是至少自己独立思考 30 分钟或者把所有自己想到的解法都写一遍之后再去看题解。如果一上来就看答案收获会大打折扣。看题解时重点不是看别人的代码而是看别人的思考路径。很多题解会写为什么想到用哈希表、为什么用两遍哈希表而不是一遍、空间复杂度能再优化吗。这些内容比代码本身值钱得多。看完之后合上题解自己重新写一遍直到能流畅地做出为止。这个过程也叫提取练习对巩固记忆非常有效。另外我建议给这道题建一个笔记记录你第一次做时的思路、错误、反思以及看完题解后的收获。LeetCode 的讨论区、精华题解、相关周赛题目都可以作为笔记素材。当你在 LeetCode 周赛 430 或者其他场次里再遇到两数之和的变体时翻看笔记会发现自己理解得特别快因为这套底层模型已经被你内化了。6. 个人体会两数之和带给我的刷题方法论6.1 把暴力解到最优解的思考路径固化下来这道题对我最大的帮助不是 AC 之后的那点成就感而是让我形成了一套分析问题的方法论。遇到任何新题我现在会先问自己最暴力的解法是什么暴力解慢在哪个环节有哪些数据结构可以让这个环节变快空间换时间是否划算比如遇到滑动窗口类题目我会想到用双端队列遇到需要频繁查询前缀和的题目我会想到用哈希表记录历史状态。这些能力的起点都可以追溯到两数之和带来的遍历时用哈希表记录已见元素这一模式。这个模式在后来的很多题目里反复出现比如判断链表中是否有环、找到数组中的重复数、连续子数组和等问题。本质上两数之和是在教我们一件事当内层循环只是为了查找某个目标值的时候可以试着用哈希表把查找时间从 O(n) 降到 O(1)。这个思想比任何模板都重要。6.2 刷题不是背题而是建立自己的知识网络很多人刷 LeetCode 追求数量一天刷十几道但过了几天全忘了。我自己的体会是刷题不应该追求做过而应该追求搞懂。搞懂一道经典题比囫囵吞枣做十道题更有用。两数之和就是一个极好的例子。从这一道题出发你可以延伸出有序数组版、设计数据结构版、三数之和、四数之和、最接近的三数之和等一系列问题。LeetCode 热门 100 题中很多题目都是这样彼此关联的与其孤立地刷不如画一张知识点网络把相关的题目串起来。如果你刚踏上 LeetCode 刷题之路或者准备面试但基础不牢我建议你把这道题放在学习计划的第一位并且用我上面提到的步骤反复练习先暴力解再哈希表再思考变体和边界条件最后在 LeetCode 周赛或热门题单里找相关题目验证。等你把两数之和彻底吃透你会发现自己看其他题目的眼光都不一样了。最后再分享一个我自己常用的训练方式隔一周之后不打开任何资料重新在编辑器里把这道题从零写一遍并口头解释每一步的意图。这种复现式刷题比单纯刷题数更能检验自己是否真正掌握。如果你能轻松做到那么恭喜你两数之和这道关你已经真正过了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询