
1. 题目到底在考什么先看懂两数之和的本质两数之和Two Sum在 LeetCode 上是编号第一的题编号第一不代表最简单而是因为它是最经典的“入门第一课”。它的描述非常短给定一个整数数组nums和一个整数目标值target请在数组中找出和为目标值的那两个整数返回它们的数组下标。每种输入只对应一个答案但是数组中同一个元素不能在答案里重复出现。很多新手第一次看到这题的时候第一反应是“这不就是两层循环嘛”然后 5 分钟写出来提交通过觉得自己会了。但实际上面试里这道题能挖的深度比你想象中大得多暴力解法的时间复杂度是多少能不能优化优化思路是什么为什么用哈希表而不是排序加双指针如果要求返回所有组合怎么做如果数组是有序的有没有更简单的写法如果 target 是负数怎么办数组里有两个相同的数怎么办这些都是在“两数之和”这个简单外壳下面藏着的真实考点。你可以把它理解为算法题里的“起步桩”——它不是为了难倒你而是为了考察你有没有基本的算法思维怎么从暴力解法出发逐步优化到更优解并且能清晰讲出每一步的理由。另外说个题外话很多人以为 LeetCode 热门 100 题里的题都是难题其实排序靠前的往往是“看起来简单但能延伸出大量知识点”的题两数之和就是最好的例子。LeetCode 周赛里偶尔也会出现两数之和的变体比如 430 场周赛里就有类似“两数之和但带限制条件”的题目本质上还是这套思路。这道题适合谁来学不只是准备面试的应届生还包括所有想建立算法思维、想搞懂哈希表实际应用、想理解“空间换时间”这句话到底什么意思的人。哪怕你工作多年不写算法看完这篇也会有收获因为里面涉及的思路——用查找表减少遍历次数——在业务代码里也很常见。2. 从暴力破解开始为什么说 O(n²) 也能过2.1 暴力解法的完整思路和代码先写最直觉的解法。外层循环枚举第一个数内层循环枚举第二个数判断两个数相加是否等于 target。要注意的是内层循环从i 1开始避免同一个元素被用两次也避免出现i和j互换后重复判断的冗余。def two_sum(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 []这段代码在 LeetCode 上其实也能通过因为题目给的数据规模通常不大。但它的问题很明显时间复杂度是 O(n²)。如果数组长度是一万那最坏情况下要比较五千多万次如果是十万那就是五十亿次。放在真实场景里这种写法基本是跑不动的。2.2 为什么面试时第一步先写暴力解很多人在面试时有个误区想一步到位写出最优解结果卡在思考过程中20 秒不说话面试官印象直接打折。我自己的经验是先快速给出暴力解法然后把它的复杂度分析说清楚再告诉面试官“我们还能怎么优化”。这是一个“展示思维过程”的策略比直接甩出最优解更能体现工程思维。暴力解本身也有值得讲的点i 1这个起点为什么重要因为如果 j 也从 0 开始你会把(0, 1)和(1, 0)判断两遍更严重的是当i j时你会在同一个元素上“自己加自己”。题目明确规定同一个元素不能重复使用这个细节就是边界条件的雏形。复杂度分析也要说完整时间 O(n²)空间 O(1)。空间是 O(1) 是因为除了存输入数组之外没有用额外的数据结构。这为后面的优化提供了一个对比基线。2.3 暴力解的局限在哪里暴力解的核心问题是内层循环在不停地做“查找”。在数组里逐个查找目标值这个操作本身是 O(n) 的。如果查找能变成 O(1)那总体复杂度就能降到 O(n)。这就是哈希表的切入点。你可以把这种优化思路理解为“查字典”暴力解相当于每道题都从头翻一遍词典哈希表则是先把词典里的字按拼音索引好查一次就是一步。所以暴力解的价值不在于“能用”而在于它作为对照系让你能清晰地看出每一步优化到底优化了什么。后面所有解法都围绕同一个问题能不能把“查找”这个动作变得更快3. 哈希表优化把查找从 O(n) 降到 O(1)3.1 核心思路边查边存两数之和哈希表解法的经典思路是遍历数组时对于每个数nums[i]检查target - nums[i]是否已经存在于哈希表中。如果存在直接返回结果如果不存在就把nums[i]作为 key、下标i作为 value 存入哈希表。这就是“边查边存”。它之所以正确是因为只要存在一对解当遍历到这对解中的后一个元素时前一个元素一定已经在哈希表里了。这样一遍遍历就能完成。3.2 代码实现与细节解释def two_sum(nums, target): hash_map {} for i, num in enumerate(nums): complement target - num if complement in hash_map: return [hash_map[complement], i] hash_map[num] i return []注意这里的关键顺序先查后存。这个顺序是刻意的。假设数组是[3, 3]target 是 6。如果先存后查i0 时把3:0存入哈希表i1 时又查到了3:0返回[0, 1]看起来也正确。但再想一种情况数组是[3]target 是 6如果先存后查遍历 i0 时先存再查就能查到同一个元素自己返回[0, 0]这就违反了“同一个元素不能重复使用”的规则。所以先查后存能在逻辑上天然规避这个问题。3.3 复杂度分析为什么是 O(n)哈希表查找和插入的平均时间复杂度都是 O(1)。所以整个过程遍历一次数组每个元素做一次常数时间的查找和插入总时间复杂度为 O(n)。空间复杂度为 O(n)因为额外存储了一个哈希表最坏情况下要存 n 个键值对。这就是典型的“空间换时间”用额外的 O(n) 空间把时间复杂度从 O(n²) 降到 O(n)。在算法面试中这种交易几乎总是划算的因为 n 变大时时间复杂度的影响远比空间复杂度严重。举个例子n 从 1000 变到 10000暴力解的时间会增长 100 倍而哈希表解法只增长 10 倍。3.4 哈希表冲突问题要不要考虑有读者会问哈希表理论上是 O(1)但如果发生大量哈希冲突不是会退化吗在竞赛或面试场景下你可以用更严格的说法平均 O(1)最坏 O(n)。但实际工程中主流语言的标准库哈希表实现都有冲突处理机制链表法、红黑树优化等在可控数据范围内基本不会退化。面试时主动提这一点会加分说明你不是只知道背模板而是理解底层。4. 边界条件与语言细节那些容易翻车的坑4.1 返回下标还是返回值两数之和原题要求返回下标这是最容易忽略的点。如果你刷过其他“两数之和”系列比如先排序再做的题目返回的往往是数值本身。下标和值是两套不同的逻辑前者要求你不能打乱原数组顺序后者允许你排序后操作。所以拿到题第一件事看清返回什么。这题的“坑”在于哈希表解法天然保存了下标信息而排序双指针法如果直接使用就会丢失下标对应关系。很多人在迁移解法时栽跟头就是把“返回值”的题用“返回下标”的思路做了。4.2 负数场景设 target 可能为负数比如nums [-3, 4, 3, 90]target 0。哈希表解法完全不受影响因为target - num 0 - (-3) 3照常查找。暴力解也不受影响。真正需要思考的是补数这个概念是否要求 target 为正——完全不要求。4.3 有多个重复值的场景LeetCode 原题限定“只有唯一答案”但真实用例里可能有两个相同元素。前面说过哈希表解法中如果值相同后存入的 key 会覆盖先前的。比如[3, 3]target 6i1 时存入hash_map[3] 1返回结果依然是[0, 1]没问题。因为先查后存的机制保证了第一个 3 是在遍历到第二个 3 之前就被查找过了。但如果换成“先存后查”那当遍历到第二个 3 时hash_map[3] 已经被覆盖成 1返回结果就变成了[1, 1]这显然错误。所以先查后存不是可有可无的细节而是保证正确性的关键。4.4 Java 的 Integer 缓存陷阱如果用 Java 写这道题有一个非常隐蔽的坑HashMap的 key 是Integer类型而Integer在 -128 到 127 之间有缓存。如果你用去比较两个Integer是否相等大数场景下会出问题但在哈希表中查找和插入用的是equals和hashCode所以不会有这个问题。但是如果面试官追问“两个Integer用比较是否相等”在 127 以内是 true超过 127 则可能是 false因为会自动装箱成新的对象。这个细节经常被拿来考察 Java 基础。4.5 找不到答案时返回什么原题保证有解但工程实现中还是要处理无解情况返回空数组或None。如果你在生产代码里写一个可能越界访问的解法那是灾难。面试时返回空列表[]是常见做法同时要说清楚“如果题目保证有解这里也可以不处理”。5. 举一反三两数之和的变体与面试延伸5.1 变体一输入是排序数组如果把输入数组改成有序的就可以用双指针法时间复杂度 O(n)空间 O(1)。左指针指向开头右指针指向结尾每次比较两数和与 target 的大小和太小则左指针右移和太大则右指针左移。这个方法的核心是“有序”带来的单调性不需要额外的哈希表。def two_sum_sorted(nums, target): left, right 0, len(nums) - 1 while left right: current_sum nums[left] nums[right] if current_sum target: return [left 1, right 1] # 注意有的题目要求下标从 1 开始 elif current_sum target: left 1 else: right - 1 return []这个小变体非常有价值因为 LeetCode 上专门有“两数之和 II - 输入有序数组”这道题解法就是双指针。面试官喜欢在追问里不断加条件先问你无序怎么做再说“如果有序呢”从哈希表到双指针的切换能看出你的底层理解。5.2 变体二三数之和两数之和延伸出去就是三数之和在数组中找到三个数使它们之和为 0。这道题的经典思路是先排序然后固定一个数剩余两个数用双指针查找。三数之和比两数之和多了一个“去重”的逻辑这也是面试的高频题。从两数之和到三数之和的进阶路径非常顺先掌握哈希表找两数再理解排序加双指针找两数最后套进三数之和你会发现大部分思路都能复用。LeetCode 热门 100 题里三数之和紧跟两数之和之后就是这个原因。5.3 变体三返回所有不重复组合如果题目改成“找出所有和为 target 的不重复组合”哈希表解法需要小心处理重复元素。这时候更稳妥的方案是先排序再用双指针并且跳过重复的元素。因为哈希表一旦遇到多个相同值key 就会覆盖丢失“哪些下标”的信息。这个变体在真实业务里更常见比如找出一组订单里能凑成某个金额的所有组合。工程问题上唯一答案的假设往往是理想化的能处理重复和枚举全部组合才是常态。5.4 变体四BST 版本的两数之和LeetCode 上还有一道题给定一棵二叉搜索树和一个目标值判断树中是否存在两个不同节点之和等于目标值。解法通常是哈希集合加递归遍历或者双指针中序遍历。它考察的是数据结构的底层遍历知识也是两数之和思路向其他数据结构迁移的样板。5.5 变体五最多一次交易的股票问题另一个从“两数之和”思路迁移过来的经典题是“买卖股票的最佳时机”给定股价数组选择某一天买入之后某一天卖出求最大利润。它本质上是找max(nums[j] - nums[i])其中 j i这跟两数之和一样都是“一前一后配对”的问题只不过条件从“加和为 target”变成了“差最大”。这类“配对型问题”是面试题库里的常客。你掌握了“遍历时用查找表记录历史信息”的思想后很多题都能秒破。6. 刷题路线与这张题单怎么用6.1 第一题的标准刷法如果你是刚开始刷 LeetCode我建议的流程是先自己尝试写暴力解提交通过后再想优化方案。不要一上来就看题解因为“自己思考过一遍”和“直接看答案”的记忆深度完全不一样。这就像学游泳看一百遍教程不如自己下水扑腾一次。两个解法都写完以后对比它们的复杂度用笔写出过程推导。别嫌麻烦算法思维的建立就是靠这种“主动产出”而不是“被动吸收”。6.2 从热门 100 题到周赛的进阶路径刷完两数之和后按顺序刷这些题比较顺三数之和、最接近的三数之和、四数之和、两数之和 II输入有序数组、两两交换链表中的节点配对思路、和为 K 的子数组前缀和 哈希表。你会发现搜索和查找表的思想无处不在地出现。等到能稳定写完这些基础题就可以开始打周赛了。LeetCode 周赛 430 场之类的新题经常是两三道基础题的组合变形。基础题的“底子”打不牢周赛里就会觉得每道题都见过但都想不出解法。6.3 面试时这一题要讲多久两数之和在面试中出现时面试官通常不会让你五分钟结束而是会听你的思考路径。我建议的节奏是30 秒讲暴力解思路2 分钟写代码30 秒讲复杂度2 分钟讲哈希表优化思路再花 2 分钟写优化代码最后留 1 分钟讨论边界和延伸题。整体控制在 8 分钟左右这是最舒服的节奏。一个常见失误是只甩出最优解然后闭嘴。面试官很想看到的是“你如何从朴素想法一步步演化到最优解”哪怕你已经知道最优解也要假装思考一下说出你排除了哪些方案以及为什么排除。这不是让你演戏而是展示真实的工程决策习惯。7. 我踩过的坑与个人体会刷题这么多年我在两数之和上踩过的坑还不少。最开始用 Python 写的时候我不知道哈希表可以用enumerate同时拿下标和值而是先range(len(nums))再nums[i]代码啰嗦不说还容易在长数组里看走眼。后来改用enumerate清爽很多。还有一个容易犯的错就是“先存后查”。我第一版写的代码是先hash_map[num] index再查补数结果在数组只有一个元素且等于 target 一半的时候返回了[0, 0]提交直接报错。从那以后我把“先查后存”当作一条铁律每次都先想清楚这个顺序为什么重要。另外我想特别强调一点刷题不只是为了面试更是为了建立一套“如何把流程优化得更快”的思维方式。两数之和里的哈希表思路放到业务代码里就是常见的“先建索引再查询”放到日志分析里就是“用字典聚合再统计”放到数据处理里就是“空间换时间”。这种迁移能力才是刷题真正的收获。如果你刚开始刷 LeetCode把两数之和当作一个起点就好后面的路还很长但这一题值得你花上半天慢慢咀嚼。能把一道简单题的每个细节都讲透比囫囵吞枣刷十道难题有价值得多。