两数之和到三数之和:哈希表与双指针的思维进化

📅 发布时间:2026/10/7 10:28:08
两数之和到三数之和:哈希表与双指针的思维进化
1. 这两道题为什么值得专门做一篇笔记先说个有意思的现象很多刷 LeetCode 的人第一周斗志最旺盛打开题库挑最简单的题开刷结果挑中的往往是“两数之和”。然后刷完这一题就兴冲冲地跑到社区发帖“AC 了打卡。”过了几天刷到“三数之和”直接卡住心态崩了帖子变成“为什么两数之和能做三数之和就不会了”这个场景出现得太频繁了以至于我每次看到都觉得两数之和和三数之和放在一起讲其实是算法入门里最被低估的一课。它俩看起来是两道题本质上是同一个“找目标组合”问题族但解法思路的转折点恰好卡在从“哈希表的时代”跳到“排序加双指针的时代”这个分水岭上。刷题的人如果只是背答案很容易在这里瞎掉。说真的基础算法精讲系列我最早想动的题目不是这两道而是二分查找。后来重新备课的时候改了主意。因为二分查找再基础它考验的还是“在一个单调序列里定位某个值”这个单一能力而两数之和、三数之和这一组题考验的是对暴力枚举的优化思维、哈希表的空间换时间、排序预处理带来的结构性收益、双指针的移动规则、去重的边界条件。一题串起来的核心概念太多了非常适合当整个系列的第一篇。在写这篇笔记之前先明确一下刷题的目标人群。这篇笔记不是写给那种已经能默写二叉树遍历、前缀和信手拈来的选手看的而是写给以下这些人的刚刚决定开始刷题但翻开两数之和题解区满屏都是“哈希表一次遍历”却看不懂为什么要一次遍历的初学者已经背过两数之和代码但换个问法比如返回所有不重复组合就懵掉的半桶水准备面试前系统过一遍双指针题型的同学需要一个能把两数之和和三数之和完整串起来的脉络。这篇文章的定位是“笔记”不是“题解搬运”。我会把每一步为什么这样做讲透代码用 Python3 写并且会把我在实测中踩过的坑、以及讲解时学生最容易问的问题一并放进来。整篇下来大约能覆盖这一族题里 80% 的思维底层逻辑。2. 为什么我把两数之和放在基础算法精讲第一题一道题的思维范式转换2.1 暴力解法不是“解法”是一个需要看穿的诅咒先把这个题目摆出来。题目本身简洁到不像一道算法题给定一个整数数组nums和一个整数目标值target请你在该数组中找出和为目标值的那两个整数并返回它们的数组下标。看到这道题条件反射式的做法是双循环def two_sum_brutal(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]这段代码没有任何语法问题测试用例小的时候也能过时间复杂度是 O(n²)空间复杂度是 O(1)。如果这是大学数据结构课的实验题交上去拿个及格没问题。但这是算法刷题它考的不是你会不会写循环嵌套而是你能不能意识到一件事内层循环做的“查找”工作是一次可以被彻底消除的重复劳动。为什么说是“重复劳动”仔细盯住内层循环它在做什么它从i1到n-1一个一个看nums[j]是不是等于target - nums[i]。也就是说对于每一个i你都在做一次线性扫描。如果这个数组有 10 万个数外层 10 万次每次扫描平均 5 万次算下来是 50 亿次比较。而其中绝大多数比较当你扫到某个值的时候你当下只关心一个问题这个值我见过没有你根本不关心它在数组的什么位置出现过——不准确地说你关心它的位置但你关心的是“能不能快速知道它的位置”。暴力解法最痛的点就在这里它把“值”和“位置”绑定得太死了。你想找的是值最后要返回的是位置但你的扫描方式让“检查一个值是否出现过”这个操作变得跟数组长度成正比。数组越长越拖沓。这个思维上的转变是这道题真正的考点你要学会把“查找某个元素是否存在”的代价从 O(n) 降低到 O(1)。而做到这一点的工具就是哈希表。2.2 哈希表的引入空间换时间是有代价的Python 里的dict就是哈希表的核心实现。它给你提供的核心能力是给定一个key平均 O(1) 时间返回对应的value。注意是平均 O(1)极端情况下哈希冲突非常严重可能退化但竞赛和面试环境里Python 的dict几乎是稳定的 O(1) 查找。回到题目这次我们不搞双循环改成一个循环def two_sum_hash(nums, target): seen {} for i, num in enumerate(nums): need target - num if need in seen: return [seen[need], i] seen[num] i我来讲讲这段代码的执行逻辑因为初学者经常在“先查字典还是先存字典”这个问题上绕晕。用示例来演示假设nums [2, 7, 11, 15],target 9。第一轮i 0, num 2。算need 7去seen里找有没有 7没有。于是把2 - 0存入seen。第二轮i 1, num 7。算need 2去seen里找发现有它的下标是 0。于是返回[0, 1]。注意这段代码里我每次都是先查再加。为什么要先查因为要找的是“两个数和为 target”如果我先把自己加进去了当num等于target / 2的时候就会查到自己。比如nums [3, 3], target 6这种用例如果你先存后查第一轮你就可能返回[0, 0]这显然是错的。先查后存保证每次查到的都是“自己之前已经遍历过的元素”逻辑上绝对安全。哈希表解法的时间复杂度是 O(n)空间复杂度是 O(n)。这就是典型的空间换时间我把之前看过的元素全部记在一个“本子”上以后每个新元素只需要翻一下本子就知道能不能配对。这段的原理讲完有人会问这不是很简单的道理吗为什么这么多人卡在这里因为很多人在写暴力解法的时候脑中的循环结构是“我找你和别人”而哈希表解法要求你把循环结构改成“我看一个记一个再问一个”。这个“状态”的维护是初学者的第一道坎也是后面所有高效算法的共同雏形——做一件事的时候沿途把信息记录下来后面再用。滑动窗口的窗口状态、前序遍历的路径记录、并查集的集合合并全都是这个思路。3. 两数之和的Python3实现细节性能、边界和面试高频变形3.1 三种写法的对比和取舍两数之和这道题最坑的是它在 LeetCode 上有个硬性要求不能使用两次循环的暴力解法来糊弄且题目要求返回下标。基于这个要求可以用的写法其实有好几套我在这里把它们都列出来方便对照选择写法时间复杂度空间复杂度适用场景返回值暴力双循环O(n²)O(1)数据量极小下标两遍哈希表O(n)O(n)需要先构建完整映射下标一遍哈希表O(n)O(n)常规最优解下标排序双指针O(n log n)O(1)不算排序空间需要返回组合值时常用值两遍哈希表的思路是先把所有元素的下标存进字典然后第二遍遍历时查。它比一遍哈希表多一次完整遍历逻辑上更直白但缺点是如果数组里有重复元素第二次查的时候需要小心取到的下标是不是自己。比如nums [3, 3], target 6第一遍构建字典的时候3 这个 key 会被后面的 3 覆盖最后字典里存的是3 - 1。第二遍遍历到第一个 3 的时候查need 3得到下标 1返回[0, 1]凑巧也是对的。但如果你遍历到第二个 3查到的下标还是 1返回[1, 1]就错了。两遍哈希表必须加一个判断查到的下标不能等于当前下标。一遍哈希表就没有这个烦恼因为它天然规避了“查到自己的问题”。面试里我自己更推荐写一遍哈希表版本代码短逻辑闭环且能展示你对状态更新的理解。如果你在面试白板上写两遍哈希表面试官多半会追问一句“能不能只遍历一次”——与其被追问不如直接主动上最优解。3.2 返回值变形LeetCode 167 和“返回所有组合”的区别有一种很常见的面试追问如果题目改成“返回两个数的值本身而不是下标”你要怎么改其实逻辑完全不用动只是return [seen[need], i]改成return [need, num]就行。但如果这道题改成“请你返回所有和为 target 的不重复二元组”难度就上来了因为去重逻辑出现了。我直接给出代码def two_sum_all_combo(nums, target): nums.sort() res [] left, right 0, len(nums) - 1 while left right: cur nums[left] nums[right] if cur target: res.append([nums[left], nums[right]]) while left right and nums[left] nums[left 1]: left 1 while left right and nums[right] nums[right - 1]: right - 1 left 1 right - 1 elif cur target: left 1 else: right - 1 return res注意这个代码里一旦找到一组目标组合我先去重再同时移动两个指针。为什么不能只动一个因为如果你找到一组后只动 left那 nums[right] 不变新的 nums[left] 和 nums[right] 的和一定大于 target因为数组是有序的nums[left] 变大了这个思路在实际调试中会很容易造成指针越界或者死循环。所以匹配成功时两个指针必须同时移动。这道变体题其实就是三数之和的前置我先放在这里就是为了让你感受到排序之后双指针操作起来有多顺也为下一节的主要内容做铺垫。3.3 关于 Python3 字典的一个实测小细节每次讲这道题我都会收到至少一个学生问为什么用if need in seen判断而不是直接用seen.get(need)这两种写法在功能上差不多但in操作对于 Python 的dict来说是直接操作哈希索引不涉及函数调用开销而.get是一个方法调用即使内建方法很快在 LeetCode 的千万级测试数据下差异也会被放大一点点。作为刷题习惯我建议能写in就写in保持判断的语义最清晰。另外seen这个名字也比map、hashtable更直观——它就是“已经看过的元素”。4. 三数之和的排序双指针同族不同法的关键转折4.1 为什么不能直接套用两数之和的哈希套路三数之和的正题是这样的给定一个整数数组nums判断是否存在三元组[nums[i], nums[j], nums[k]]满足i、j、k互不相同且nums[i] nums[j] nums[k] 0。注意此题明确要求返回所有不重复的三元组而不是下标集合。也就是说结果里不能出现重复的三元组比如[-1, 0, 1]和[1, 0, -1]算是同一种组合。新手拿到这题的第一反应就是套上一题的思路遍历一个数剩下的两数之和问题交给哈希表。这个思路本身没错但会撞上两个很致命的坑。第一个坑哈希表天然不在乎顺序。两数之和之所以能用哈希是因为返回下标时顺序是无所谓的[i, j]和[j, i]在题目语义上是同一个答案但三数之和要求输出组合值哈希表去重的代价会非常高。你得把每个满足条件的三元组都找出来然后排序再塞进一个set里最后还要防止排序后的元素顺序不同导致重复计算整个流程充满了不必要的复杂度。第二个坑找三元组的数量是组合级的。两数之和只需要找到一个答案就可以返回而三数之和要把所有答案全找出来。哈希表在这件事上能办到但去重逻辑会写得让你怀疑人生。我来给你展示一下“纯哈希去重流”的问题假设数组是[-1, -1, 0, 1]你用哈希表找两数之和-1和1会组队但你无法在遍历时快速判断“当前这个-1和之前那个-1属于同一位置”或者更准确地说你判断的代价太大。所以这个问题的正确打开方式是换一个完全不同的思路先排序再用双指针。4.2 排序带来的结构性收益双指针为什么能减少循环层级排序这个预处理操作在暴力解法里看起来是“多此一举”但在区间搜索问题里它是划时代的优化手段。为什么因为一旦数组有序你就可以利用单调性来快速判断指针该怎么走。我先说结论框架def three_sum(nums): nums.sort() n len(nums) res [] for i in range(n - 2): if i 0 and nums[i] nums[i - 1]: continue if nums[i] 0: break left, right i 1, n - 1 while left right: total nums[i] nums[left] nums[right] if total 0: left 1 elif total 0: right - 1 else: res.append([nums[i], nums[left], nums[right]]) while left right and nums[left] nums[left 1]: left 1 while left right and nums[right] nums[right - 1]: right - 1 left 1 right - 1 return res在这个代码里外层遍历是固定第一个数nums[i]内层用双指针在i1到n-1的区间里找两个数让三者之和为零。我们来过一遍核心逻辑。整个数组已经从小到大排好序了。考虑某个固定的i此时left指向区间最左端right指向区间最右端。计算nums[i] nums[left] nums[right]然后看总和总和小于 0说明当前三个数的和太小了需要增大。左边指针往右挪一位也就是left 1。总和大于 0说明太大了需要减小。右边指针往左挪一位也就是right - 1。总和等于 0找到一个答案加入结果集然后去重、收缩区间。这个思路的巧妙之处在于双指针的移动是有方向性的。排序后nums[left]和nums[right]天然夹出一个区间你只需要不断向中间逼近这个过程里每个 left-right 组合最多被访问一次因此内层双指针的复杂度是 O(n)外层循环还有 O(n)总体是 O(n²)。对比暴力三重循环的 O(n³)这是质的飞跃。4.3 三个去重细节少了任何一个都会让你被重复答案搞疯三数之和最磨人的不是能不能想到双指针而是去重。面试里很多同学写完代码之后跑示例通过了提交却 WAWrong Answer原因几乎全在去重上。第一个去重位置是外层循环的i。如果nums[i] nums[i - 1]直接跳过。为什么因为如果当前数和前一个数相同那么固定i能产生的组合集合和固定i - 1时完全一样。你枚举了第一次就够了第二次纯粹是重复劳动。注意这里要用nums[i] nums[i - 1]判断而不是nums[i] nums[i 1]为什么如果是后者你可能会在一开始的连续重复区间里错过唯一合法的一组合法使用比如数组[-1, -1, 2]你直接用nums[i] nums[i 1]判断第一个-1时发现nums[0] nums[1]就跳过了但[-1, -1, 2]是一个合法答案。第二个去重位置是匹配成功之后的left和right去重。找到一个三元组后先把所有和当前nums[left]相等的指针统统往右挪再把所有和当前nums[right]相等的指针统统往左挪然后 left 和 right 再各走一步。这一步是为了保证下一次找目标时两个指针的两个值都不会和上一组重复。第三个细节是对首元素nums[i]的判断。这里有个很实用的小剪枝如果nums[i] 0直接终止循环。因为数组已经有序第一个数都已经是正数了后面两个数更大三个正数不可能加出 0。这段代码在实际刷题中能砍掉不少无谓的遍历尤其是数组里大正数很多的情况。三个去重捂住一个结果里一定出现重复三元组。我带着学生刷题时遇到过不下五次这种情况每次都老老实实断点看一眼才发现在去重这里漏了。5. 从两数到三数再往上走复杂度进化与一整个题族5.1 一个朴素规律N数之和的复杂度阶梯把两数之和和三数之和并排放在一起看一个规律逐渐浮现出来两数之和 哈希表O(n) 时间O(n) 空间。三数之和 排序双指针O(n²) 时间O(n) 空间排序空间视语言实现。四数之和 排序双指针两层固定O(n³) 时间O(n) 空间。这个规律说明什么说明“从 N 个数里找 K 个数使它们的和等于 target”这一类问题当 K 固定时最优解的时间复杂度大致是 O(n 的 K-1 次方)。为什么不是 O(n 的 K 次方因为你枚举前 K-1 个数最后一个数可以通过双指针或者哈希表在 O(n) 内解决而不是再套一层循环。带这个规律去刷题四数之和就变得很简单了在三数之和的外面再套一层循环。def four_sum(nums, target): nums.sort() n len(nums) res [] for i in range(n - 3): if i 0 and nums[i] nums[i - 1]: continue for j in range(i 1, n - 2): if j i 1 and nums[j] nums[j - 1]: continue left, right j 1, n - 1 while left right: total nums[i] nums[j] nums[left] nums[right] if total target: left 1 elif total target: right - 1 else: res.append([nums[i], nums[j], nums[left], nums[right]]) while left right and nums[left] nums[left 1]: left 1 while left right and nums[right] nums[right - 1]: right - 1 left 1 right - 1 return res你仔细看这代码和三数之和的区别仅仅就是多了一层固定 j 的循环以及外层去重的下标判断从i 0变成了j i 1。这就是“吃透两数之和和三数之和顺带把四数之和也拿下了”的原因。这个题族的思维递进关系清晰非常适合作为刷题路上的模板。5.2 边界情况和 Python 排序的注意事项聊完大框架我想补几个实际刷题中经常遇到的边界问题这些问题不处理代码可能在某些测试用例上直接崩。第一个边界问题是数组长度不够。三数之和要求至少有三个元素四数之和要求至少四个。如果你不提前判断len(nums) 3就返回空列表那么当nums []时nums.sort()没问题但range(n - 2)会变成range(-2)直接不执行循环倒也不会出错。但有些语言的实现里会有越界风险所以我建议刷题时都养成习惯开头就写if len(nums) 3: return []第二个边界问题是排序稳定性。Python 的list.sort()是稳定排序但三数之和这里不依赖稳定性因为我们是数值比较不是键值对排序。不过要小心如果你对元素排序后还需要保留原始下标那就要直接存下标或者用enumerate包装。第三个边界问题是全零数组。比如nums [0, 0, 0, 0]期望输出只有一个[[0, 0, 0]]。我们的去重逻辑能不能正确处理外层i 0找到第一个三元组(0, 0, 0)然后 left 去重、right 去重left 和 right 收缩循环结束外层i 1时nums[1] nums[0]跳过i 2时同理。最终只保留一个三元组完全正确。第四个边界问题就是 Python 的负数和正数分界线。由于 Python 的整数没有固定范围不存在溢出的说法所以在做nums[i] nums[left] nums[right]的时候不需要考虑整型溢出这比 C 和 Java 要省心很多。但要注意这种便利不能滥用遇到超大数的输入时即使不会溢出也可能因为求和太频繁导致常数因子变大实际运行时间变长。5.3 常见 FAQ为什么三数之和不能用哈希表直接解这个问题几乎每次讲必被问到。我把它写清楚当成一个典型误区来解剖。严格来说三数之和用哈希表是可以解的固定两个数第三个数去哈希表里查。但问题在于第一哈希表操作天然无序。找到的每个三元组需要先排序才能去重这个排序操作会让代码的时间复杂度增加一个 log 因子。第二去重逻辑极其繁琐。你必须在固定的外层循环里维护“这个索引之前有没有用过相同的值”这个状态非常容易写错。第三空间复杂度是无谓的。哈希表解法至少需要 O(n) 的额外空间而排序双指针解法只需要 O(1) 的额外空间除了排序占用的栈空间在内存越紧张的场景双指针的优势越大。所以“什么场景用哈希什么场景用双指针”的判断标准是什么呢我的个人经验是如果题目要求返回的是下标优先考虑哈希如果题目要求返回的是不重复组合值优先考虑排序加双指针。前者重位置后者重组合这个区分能帮你快速圈定方法方向。6. 把思路变成肌肉记忆的三个习惯6.1 刷这道题时建议刻意记几句“口诀式”总结我不提倡死背代码但有几句话是值得反复默诵的它们能帮你快速在脑内搭建起解法框架“两数之和哈希一遍边查边存不会自己。”核心查 need 之前先保证自己是遍历过的旧元素先查后存。“三数之和排序双指针固定一个两个移动。”核心先排序再固定第一个数用双指针在剩余区间找两个数。“匹配成功双指针一起收去重时左重跳左右重跳右。”核心找到一组答案后两个指针同时移动并把所有连续重复值跳过。这几句话在刷题过程中反复默念能极大减少调试时返工的概率。说实话我每次带同学刷 LeetCode都是先让他默写这三句话再让他默写代码。效果好得出奇。6.2 一步步跑一个真实案例比看十遍题解管用学算法最怕的就是只看不练。作为一个实操性强的建议我特别推荐你在本地跑一遍下面的测试用例nums [-1, 0, 1, 2, -1, -4]对应 LeetCode 原题示例期望结果是[[-1, -1, 2], [-1, 0, 1]]。nums [0, 0, 0]期望结果是[[0, 0, 0]]。nums [3, 0, -2, -1, 1, 2]这是我自己加的刁钻用例排序后是[-2, -1, 0, 1, 2, 3]仔细想想为什么-1和1的组合会出现在结果里。nums [1, 2, -2, -1]排序后是[-2, -1, 1, 2]由于排序后负数在前的特点双指针区间的收缩逻辑会被完整遍历。拿一组用例一行行跟踪 left、right 的移动轨迹比对着题解看十遍更有效。把指针移动的每一步都写出来你会发现双指针的“单调性”就像是一条准绳把所有不必要的路径都剪掉了。6.3 和 LeetCode 题库的联动刷完这两道后面接什么题基础算法精讲系列的第一篇我把这两道题放一起讲是因为这个题族可以一口气延伸出好几道经典题。刷完这两道按照难度递增的关系我推荐的刷题顺序是LeetCode 1两数之和哈希表入门LeetCode 167两数之和 II - 输入有序数组排序双指针轻量版LeetCode 15三数之和核心题吃透双指针和去重LeetCode 18四数之和套壳题验证你理解的是不是套路LeetCode 653两数之和 IV - 输入 BST树上的两数之和变体考验你把哈希思维迁移到树上。这几道题刷下来你对“找组合”这类问题的理解会牢固很多。尤其是第四题四数之和如果你能不看任何题解仅凭三数之和的模板自己扩展出来说明你已经真正掌握这个题族的套路了。7. 实测中容易踩到的隐形坑调试记录与教训汇总写下这些之前我先说明这一节的内容完全来自我自己的实测和带练过程中遇到的真实报错与踩坑不是凭空想象。每个坑都有对应的调试现场。第一个坑很多人忽略了对nums为空或长度不足的判断。我在初学阶段写三数之和当nums []时代码进入for i in range(n - 2)结果是range(-2)循环体一次都不执行程序不会报错但很容易误导人。真正有问题的是四数之和n - 3如果是负数在某些写法下可能出现索引异常。建议一开头就补上长度判断这属于防御式编程面试官看到也不会觉得多余。第二个坑是外层循环的去重条件写成nums[i] nums[i 1]。前面讲过的经典错误这里再强调一遍如果数组里有连续相等的数用i 1去重会直接把第一个合法值也跳过了。我实测用[-1, -1, 2]跑过一次肉眼可见地丢掉了正确答案。这个错位很容易发生因为人脑直觉上会觉得“既然我用了这个数那下一个相同的数肯定重复”但没意识到第一个数才是那个不重复组合的开端。第三个坑是双指针匹配成功后只移动一个指针。我见过很多同学的代码找到一组答案后left 1就直接进行下一轮结果right不变新的nums[left]和nums[right]相加一定比上一组大导致right永远不再移动最终陷入死循环或者漏解。这是一个典型的、写出 bug 却很难一眼看出的问题。我的建议是一旦匹配成功left 和 right 的移动必须“打包处理”即去重完之后同时收缩。第四个坑是在三数之和里错误地用while nums[left] nums[left 1]去重但忘了检查left right。当数组里全是重复值比如[0, 0, 0, 0]最后一个有效区间收缩到left right之后再访问nums[left 1]就会越界。所有去重循环都必须先判断指针是否还在合法区间内。第五个坑是nums[i] 0的剪枝剪过头。有一种常见写法是if nums[i] 0 and target 0: break这个是有默认前提的三数之和的 target 是 0。如果你把代码改造成通用的“N 数之和等于 target”版本nums[i] 0直接 break 就不成立了因为 target 可能为负数。In 三数之和这道题里target 是 0所以剪枝安全但你在扩展成四数之和时必须重新审视这个条件。第六个坑是在 Python3 里使用set去重然后直接返回列表。我之前见过一个同学的解法把所有结果三元组排序后塞进set期望这样自动去重。但问题是同一个三元组[-1, 0, 1]和[-1, 1, 0]在排序之前是两个不同的 tuple塞进 set 后反而产生两条“看似不同、实则相同”的记录。正确的顺序一定是找到候选结果后对结果排序再入 set或者干脆用双指针原地去重不碰 set。后者更符合算法面试的审美因为它的空间复杂度更可控。这些坑沉淀下来之后我对学生的要求是写完代码先跑边界用例再跑重复值多的用例最后再跑正常用例。这个顺序能覆盖 90% 的 WA 原因。8. 最后的建议基础算法精讲系列后续的延伸方向这一篇是灵茶山艾府基础算法精讲系列的第一篇。很多读者看完会问这个系列接下来会覆盖什么。我的规划是这篇笔记只是整个算法框架的起点后续会依次覆盖二分查找、双指针进阶比如盛最多水的容器、滑动窗口、前缀和、差分数组、单调栈、并查集、图论基础等场景。两数之和和三数之和的核心价值在于建立“遍历 记录 查找”以及“排序 指针移动”这两种范式它们会反复出现在后面每一类题型里。具体到个人实操建议我建议刷完这篇文章后给自己定一个小目标三天之内用不查任何资料的方式把三数之和的代码完整默写两遍并把四数之和尝试独立写一遍。这是把短时记忆转成长时记忆最有效的物理手段。如果你只是“看懂了”然后合上屏幕那和没看没太大区别。顺便提一个很多人忽略的训练方法刷 LeetCode 时把每一道题的“时间复杂度推导”写一下。比如问自己两数之和为什么是 O(n)因为每个元素最多被访问多少次答案是每个元素只进入字典一次也只被查一次所以是 O(n)。这种主动推导比被动接受题解强十倍。我带过的学员里凡是能把复杂度推明白的后面刷动态规划都明显比别人顺。这道题族里还有一些值得挖的细节没有在这篇里展开比如哈希表的冲突处理机制Python 字典的开放寻址、双指针的停止条件证明、去重逻辑的严谨性证明这些内容对面试深挖非常有用。后续我会结合具体题目展开聊这里先埋个伏笔。最后再分享一个小技巧如果你在面试里遇到类似的两数之和题面试官问“还能优化吗”这样的问题时别急着回答“不能了”。多考虑一个维度如果数组是有序的题目就变成了 LeetCode 167可以用双指针做到 O(n) 时间、O(1) 空间绕过哈希表的额外空间。这种“基于条件变化而切换解法”的能力往往是决定面试成败的关键分水岭。我在实战中靠这个意识救过好几次场希望你在下次面试时也能用上。