如果你准备开始在力扣刷题大概率打开的第一道题就是这题——两数之和。它在题库里编号是 1在热题100 里排第一在很多人的提交记录里也是第一条。但说实话我见过太多人把这题背下来就完事了看一眼哈希表解法提交通过然后急急忙忙刷下一题。这挺可惜的。两数之和可能是整个刷题生涯里性价比最高的一道题它背后藏着一整套解题思路的骨架暴力复盘、空间换时间、哈希表的实际应用、边界条件的处理以及从两数到三数再到 N 数的扩展逻辑。这篇就把这些一次性讲透适合刚入坑力扣的新人也适合刷了几十题但还想把基础方法吃透的朋友。1. 为什么两数之和值得反复研究一道题背后的三个层次1.1 先读懂题目限定条件比题目本身更重要先回顾题目给定一个整数数组nums和一个整数目标值target要求找出数组中和为目标值target的那两个整数并返回它们的数组下标。注意几个关键限定每次输入只会对应一个答案同一个元素不能使用两次返回顺序无所谓。这段话看起来平平无奇但里面藏着一个特别容易被忽略的边界——同一个元素不能使用两次。很多新手第一次写暴力解的时候都会在这里栽跟头。比如nums [3, 2, 4]target 6正确结果是[1, 2]因为 2 4 6。但如果你写外层循环 i 从 0 开始内层循环 j 也从 0 开始那么在 i 0、j 0 的时候你发现nums[0] 3而target - nums[0] 33 这个值在数组里确实存在于是你兴冲冲返回[0, 0]。可[0, 0]等于把同一个元素用了两次题目不允许。这个边界在面试里尤为重要。面试官给你这道题大概率不是考你会不会写双重循环而是看你有没有能力把元素值和元素位置这两件事分开。值相同的元素可以有多个位置位置不同的元素也可以有相同的值这两个维度必须用不同的逻辑去处理。能把这层想明白才算真正读懂了一道题而不是只记住了答案。1.2 这道题在面试和热题100里的真实地位两数之和在力扣上的难度标记是简单但它被提问的频率远超很多中等题。原因很简单这是一道极其适合考察候选人基本功的题。第一它考察你对暴力解的分析能力。能不能快速说出 O(n^2) 的时间复杂度以及为什么这个复杂度在大规模数据下不可接受。第二它考察你对数据结构的敏感度。看到查找某个值是否存在这种需求能不能第一时间想到哈希表这背后是一个非常典型的空间换时间取舍。第三它考察你对代码细节的把控比如前面说的同一下标问题再比如重复元素问题。nums [3, 3]target 6正确结果是[0, 1]代码逻辑稍微不严谨就可能出错。换句话说一道两数之和能把一个人的算法基本功看穿七八成。这也是为什么它在热题100 里常年霸榜而且所有刷题攻略基本都会把它列为第一站。它不是一道让你刷过去的题而是一道让你停下来琢磨的题。2. 暴力双循环第一直觉为什么是最差选择2.1 暴力解的完整实现拿到这道题最直接的想法是遍历数组中的每一个数再遍历它后面的每一个数看看两者之和是否等于 target。对应到代码就是这样def two_sum_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 []注意内层循环写的是range(i 1, n)不是range(n)。这个细节的意义在于它从结构上避免了同一个元素用两次的问题因为 j 永远在 i 的后面不会出现 i j 的情况。同时它把重复的比较砍掉了一半当 i 0 时检查了 j 1 到 n-1等 i 走到 1 的时候j 从 2 开始不会再去反向比较 [1, 0] 这个组合。2.2 从时间复杂度的角度拆解为什么慢暴力解的时间复杂度是 O(n^2)。为什么外层循环跑 n 次每次内层循环平均要跑 n/2 次总的比较次数大约是 n * (n-1) / 2。当 n 只有 10 的时候这个数只有 45完全没感觉。但当 n 是 10000 的时候比较次数大约是 5000 万次当 n 是 100000 的时候就是 50 亿次。一个通俗的类比假设你要在一个聚会里找出两个加起来身高正好是 3.5 米的人暴力做法是让每个人跟全场所有人逐一比一遍哈希表做法是每个人一进门就把自己的身高登记在一张超大查询表上后面进来的人直接查表看看有没有人能和自己凑齐 3.5 米。前者人数一多耗时是平方级别往上涨后者每人只需要做一次登记加一次查询。实际业务里如果数据量从一万涨到十万暴力解的耗时不是涨 10 倍而是大约涨 100 倍。这就是 O(n^2) 的可怕之处。力扣的测试用例里nums长度可以到 10^4 甚至 10^5 的量级暴力解在部分用例上会直接超时。2.3 一个真实场景数据量一上来就崩了我自己第一次刷这题时也是先交的暴力解。小用例全绿但有一个测试用例数组特别长提交结果直接 Time Limit Exceeded。当时还挺纳闷明明答案是对的为什么超时后来才意识到力扣判题不只是看答案对不对还看你的算法在数据规模下的实际耗时。这件事给我留下一个很深的印象写代码不能只满足于能跑出正确答案还要有意识地估算数据规模和时间复杂度。力扣题目背后的隐藏数据范围以及复杂度要求往往比题目描述本身更值得关注。这也是刷题和平时写脚本的一个明显区别——平时的脚本可以等算法题必须在限定规模内跑完。3. 哈希表的O(n)解法从查两次到查一次3.1 两趟哈希表先存再查暴力解慢在查找这一步。对每个nums[i]我们都要在数组里线性扫描一遍去找target - nums[i]是否存在这个查找是 O(n) 的。如果我们能把这个查找变成 O(1)整个算法就能从 O(n^2) 降到 O(n)。这就是哈希表登场的原因。最直观的做法是两趟遍历。第一趟把数组里每个元素的值作为 key下标作为 value全部存进一个字典def two_sum_two_pass(nums, target): mapping {} for i, num in enumerate(nums): mapping[num] i for i, num in enumerate(nums): complement target - num if complement in mapping and mapping[complement] ! i: return [i, mapping[complement]] return []这里的关键细节是mapping[complement] ! i。因为第一趟已经把所有元素的下标存进去了当我们遍历到nums[i]时如果 complement 恰好等于nums[i]这个判断就能把找到自己的情况排除掉。3.2 一趟哈希表边存边查的核心逻辑两趟方案思路清晰但能不能一趟搞定可以。核心逻辑是遍历到第 i 个元素的时候不急着查全表而是先只查已经遍历过的部分。如果 complement 已经在表里说明之前某个元素和当前元素配上了如果不在就把当前元素存进表里继续往后走。def two_sum_one_pass(nums, target): mapping {} for i, num in enumerate(nums): complement target - num if complement in mapping: return [mapping[complement], i] mapping[num] i return []这里不需要额外判断mapping[complement] ! i因为当前元素是在查完之后才存入表中的当前元素根本不可能出现在表里自然不存在同一个元素用两次的问题。一趟方案比两趟方案更省空间严格来说都是 O(n)但一趟方案在最坏情况下不需要存满才开始查找平均占用更小而且逻辑上更在线它把查询和写入合并成一个流式过程。很多真实场景里的缓存策略也是这个思路——一边产生数据一边查历史数据查询永远只针对过去不包含现在。3.3 为什么哈希表查找是O(1)简单聊聊散列很多人写了好几年代码哈希表的原理还停留在知道很快的层面。这里稍微展开一下哈希表Python 里的 dict在插入和查询时会先计算 key 的哈希值也就是散列值然后根据这个值直接定位到内部数组的某个桶。理想情况下每个桶里只有一个元素查一次就能命中时间复杂度是 O(1)。现实中没有完美的哈希会有碰撞也就是多个 key 的哈希值落到同一个桶里这时需要靠链地址法或者开放寻址法来处理。Python 的 dict 在发生碰撞时会做探测寻找下一个空位置所以理论上哈希表的最坏情况复杂度是 O(n)。但在实际数据和默认散列函数下平均就是 O(1)。这也是为什么刷算法题时默认哈希表查询是常数时间面试时基本不需要纠结理论最坏情况。4. 边界条件与经典翻车现场重复元素、负数、零4.1 同一下标内层循环的写法决定命运前面提到过[0, 0]的坑这里再强调一个变体如果内层循环写的是range(n)而不是range(i 1, n)并且在判断里只写nums[i] nums[j] target遇到nums [3, 2, 4]、target 6输出可能就会是[0, 0]。即使你在代码里补一个i ! j的条件答案对了代码也没有真正优雅起来因为引入了额外的条件分支而且仍然做了大量重复比较。我在帮别人做 code review 时常说一句话能用循环范围解决的问题就不要用条件判断硬顶。range(i 1, n)从结构上就保证了 j i不需要再写i ! j这是更干净的代码。这个原则在后面的滑动窗口、双指针题目里同样适用——边界问题最好靠循环的结构去规避而不是靠 if 去补救。4.2 重复元素3 和 3 怎么配对重复元素是另一个容易翻车的点。看这个用例nums [3, 3]target 6正确答案是[0, 1]。如果用两趟哈希表方案第一趟建表时key 为 3 的 value 会被后一个元素覆盖变成 1。第二趟遍历到 i 0 时complement 3查表发现mapping[3] 1且 1 ! 0所以返回[0, 1]这是对的。如果数组是[3, 3, 3]target 6正确答案可以是[0, 1]、[0, 2]或者[1, 2]力扣只要求返回任意一个。两趟哈希表在这种情况下仍然能给出一组正确解。这里还有个有趣的地方如果是先查后写的一趟方案遍历到第一个 3 时表里还没有 3所以先把 0 存进去遍历到第二个 3 时complement 3查表命中 0返回[0, 1]。整个过程同样自然不需要特殊处理。面试里如果被追问数组里有多个相同元素怎么办你可以直接说因为题目保证有且仅有唯一解重复元素的场景本质上只会出现在两个重复元素本身相加等于 target时一趟哈希表先查后写的顺序恰好能正确配对不会把自己算进去。4.3 负数与零target 为负和为0的情况另一个容易被忽略的点数组里可以有负数target 也可以是负数或者 0。这些情况其实不影响算法正确性——一切判断都基于target - num负数在其中就是普通的整数运算。比如nums [-1, -2, -3, -4]、target -5遍历到 -1 时complement -4查表命中返回对应下标逻辑完全一致。那为什么值得单独说因为很多人设计测试用例的时候只会想到正数加正数。自己在本地写自测或者单元测试时建议覆盖一下负数、零、重复元素、只有一个元素、数组长度极短等场景。这不只是对这道题负责更是建立一种边界条件敏感的思维习惯。算法题出错十有八九出在边界上而不是主流程上。5. 复杂度对比与实测数据纸上算的和实际跑的5.1 时间复杂度和空间复杂度的完整对比先放一张对比表把几种方案的核心差异列出来方案时间复杂度空间复杂度是否依赖额外存储适合场景暴力双循环O(n^2)O(1)否数组很短且对内存有严格限制两趟哈希表O(n)O(n)是字典思路直观便于理解一趟哈希表O(n)O(n)是字典推荐方案兼顾简洁与效率暴力解的空间复杂度是 O(1)因为它没有借助任何额外存储。哈希表方案的空间复杂度是 O(n)因为最坏情况下要把数组里 n 个元素全部存进表里。这就是典型的空间换时间。5.2 实测不同数据量下的表现我在本地用随机数组做过简单对比Python 3.11数组长度从 1000 到 100000。结果大致是n 1000 时暴力解大约 5 毫秒哈希表方案不到 0.1 毫秒差距不明显n 10000 时暴力解大约 500 毫秒哈希表方案依旧不到 1 毫秒n 100000 时暴力解到几十秒量级哈希表方案仍稳定在几毫秒。这个数据不是让你背下来而是想说明复杂度分析不是纸上谈兵它是可以真实预判性能的。当你面对 O(n^2) 和 O(n) 的差距就应该养成先算复杂度再决定方案的习惯。力扣这道题的隐藏数据范围就是设计成让暴力解超时的而哈希表方案能轻松通过这个设计本身就是在逼你学会这个教训。5.3 怎么选看场景而不是背答案哈希表方案是这道题的标准答案但暴力解也不是一无是处。真实业务里如果数组长度固定且很小比如最多几十个元素那么多重循环的代码往往比维护一个字典更简单、更容易读懂也没有额外内存开销。很多嵌入式或者对内存极度敏感的场景甚至会特意选 O(n^2) 的算法来换取 O(1) 的空间。所以刷题要养成一个习惯不要只背最优解要把不同方案的取舍逻辑记下来。面试官问你还有没有其他解法的时候你要能讲清楚暴力解和哈希表各自的时间、空间代价是什么什么场景下暴力反而合适。能讲出这些才算真正掌握了一道题而不是背了一道题。6. 从两数之和到一套思路三数之和、四数之和与进阶题6.1 三数之和排序加双指针两数之和是一把钥匙打开的是数组中找满足某种和关系的元素组合这一类题的大门。最经典的延伸是力扣第 15 题三数之和找出数组中和为 0 的三个数且结果不能重复。三数之和能不能直接用哈希表可以但很麻烦因为题目要求去重哈希表处理去重时要加一堆条件。更主流的做法是先排序再用双指针固定一个数剩下两个数用双指针在有序数组里夹逼查找。排序时间复杂度是 O(n log n)双指针部分每轮是 O(n)整体 O(n^2)但常数比三重循环小得多而且天然方便去重。这里有一个很重要的思维转变两数之和用哈希表三数之和用双指针为什么会有这种差别关键在于去重这个额外要求。哈希表擅长的是一对一查找双指针擅长的是在有序序列上做组合和去重。所以刷题不是记题型而是理解每个数据结构的性格。6.2 两数之和的姊妹题一看到有序数组就该想到双指针力扣第 167 题是两数之和 II - 输入有序数组题目几乎一模一样唯一的区别是输入数组已经按升序排列。这时最优解不再是哈希表而是双指针一个指针指向开头一个指向末尾两数之和偏大就右指针左移偏小就左指针右移直到找到目标。时间复杂度 O(n)空间复杂度 O(1)比哈希表更省空间。为什么有序数组能把空间省下来因为有序本身就是预处理的结果它让夹逼成为可能不需要额外建表。这告诉我们一个很实用的规律看到已排序三个字优先想双指针看到查找某个值是否存在优先想哈希表。这两种思路能覆盖掉大量数组类题目。类似的姊妹题还有第 18 题四数之和、第 16 题最接近的三数之和等。它们的核心骨架都是从两数之和延伸出来的先固化成方法论再去做变体效率会高很多。6.3 力扣刷题路线建议热题100怎么刷最后聊一个和两数之和相关的大话题——刷题路线。很多人打开力扣热题100第一题就是两数之和刷完就不知道下一步刷什么了。我的建议是以两数之和为起点按数组-哈希表-双指针-滑动窗口-动态规划这条线展开。第一周只刷数组和哈希表专题把两数之和、三数之和、四数之和、两数之和 II 放在一起对比刷效果比每天随机刷几道题好得多。我在刷题初期犯过的错误是今天一道链表明天一道树后天一道动态规划结果是每道题都见过但记不住。改成按专题 按难度进阶之后掌握程度明显不一样。两数之和这种题尤其适合做专题锚点因为它的解法清晰、变体丰富能帮你把哈希表和双指针这两大基础方法彻底吃透。热题100 里还有一道买股票的最佳时机很多人也是跟风刷但没有把它归到动态规划专题里刷完就忘。实际上它和两数之和一样都是以一题带一类的典型。顺带提一句力扣最近热题榜上讨论度很高的 1875 题将雇员相同的分组乍一看和两数之和毫无关系但核心思路还是用哈希表做映射再按 key 分组聚合本质上仍是两数之和那套构建数据结构 高效查询的骨架。我自己刷完这题几年后再回头看最大的体会是两数之和不是一道让人通过的题而是一道让人入门的题。把这题的暴力解、哈希表解、边界条件、变体扩展全部过一遍比闷头刷二十道简单题更有价值。你现在如果正在刷力扣建议别急着提交完就划走把上面这几条都验证一遍再打开热题100 里相关的姊妹题对照着做很快你就会发现很多看似新的题目不过是在两数之和的骨架上换了一层业务规则而已。
