news 2026/10/24 20:54:40

三数之和双指针解法:去重细节与算法复杂度优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
三数之和双指针解法:去重细节与算法复杂度优化

刷题刷到 LeetCode 15 题“三数之和”,这个位置非常微妙。前十几题基本是数组、字符串、动态规划热身,而这一题一出来,很多人的思维会卡住。题目本身不复杂:给定一个整数数组,找出所有和为 0 且不重复的三元组。但“不重复”三个字是真正的杀手,它逼着你从“能不能做出来”跨到“怎么做才能又快又对”。作为经典中的经典,这道题集中考察了排序预处理、双指针收缩、哈希去重思路,以及最重要的——写代码时的边界感。无论你是刚开始准备面试,还是已经刷了一百题想补漏,三数之和都是一道必须吃透的题。

我当年第一次做这题时,第一反应是三重循环暴力解,当场被时间复杂度教做人。后来学乖了,老老实实排序加双指针,才体会到什么叫“原来思路是这么顺的”。这篇总结我会把从暴力到双指针的完整思考过程、去重细节、常见错误、代码实现以及扩展题型全部聊透,保证你读完能直接上手写,也能应付面试官的追问。

1. 先看清问题本质:三数之和到底在考什么

1.1 题目描述与原题拆解

题目给的是一个整数数组nums,要求返回所有满足nums[i] + nums[j] + nums[k] == 0的三元组,并且三元组之间不能重复。注意几个容易忽略的条件:

  • 同一个元素不能重复使用,也就是说i、j、k是三个不同的下标。
  • 三元组内部顺序无关紧要,[-1,0,1]和[1,-1,0]被视为同一个三元组。
  • 结果中不能包含重复三元组,比如数组里有两个-1,只能输出一个[-1,0,1]。

这类问题最让人头疼的就是去重,因为如果只要求找到任意一组,三重循环或者哈希直接就出来了。难就难在“所有”和“不重复”这两个词上。

原题举例:nums = [-1,0,1,2,-1,-4],正确结果是[[-1,-1,2],[-1,0,1]]。注意[-1,0,1]出现了两次吗?并没有。因为数组里有两个-1,但题目只允许输出一个三元组,这里就暗含了去重逻辑:你要跳过重复元素,而不是把重复组合都塞进结果。

题目本身的核心逻辑其实是在做“组合枚举”,要求你枚举所有三元素组合,但组合之间不能有重复。简单粗暴的组合枚举是 C(n,3) 种可能,n 一旦到几百,三重循环就废了。

1.2 为什么这题是面试高频题

三数之和在面试题里出现频率极高,原因有三个。

第一,它有一个非常清晰的最优解法——排序加双指针,这属于基础算法里“预处理 + 相向双指针”的经典套路。面试官希望看你能不能想到先排序,能不能把 O(n³) 降到 O(n²)。

第二,去重逻辑设计考察的是代码细节。很多候选人能写出双指针主体,但去重条件写错,导致结果里有一堆重复三元组。这个问题能瞬间拉开代码能力差异。

第三,这题可以自然衍生出很多变体:最接近的三数之和、四数之和、删除重复元素后的组合数等等。会了这道题,后面的四数基本是复制粘贴再加一层循环。面试官可以用一道题考察好几个层级的水平,性价比极高。

所以,与其说这道题在考“你会不会双指针”,不如说它是在考“你能不能把简单的东西写严谨”。

2. 从暴力解到双指针:完整思维演进

2.1 最直观的暴力三重循环

看到三数之和,第一反应自然是三重循环。直接写的话,长这样:

def threeSum(nums): n = len(nums) res = [] seen = set() for i in range(n): for j in range(i + 1, n): for k in range(j + 1, n): if nums[i] + nums[j] + nums[k] == 0: triplet = tuple(sorted([nums[i], nums[j], nums[k]])) if triplet not in seen: seen.add(triplet) res.append(list(triplet)) return res

这个版本能保证结果不重复,因为每个三元组排序后转元组放进 set。但问题太明显:时间复杂度是 O(n³),空间复杂度也用了 O(n) 的 set。当 n=3000,直接超时。

暴力解的意义在于帮你理解问题本身,而不是作为最终方案。你可以仔细想想,暴力解里哪些计算是重复的。三个数求和,内层循环做了大量的无用功,因为后两个数的组合根本不需要全量枚举。我们能不能固定一个数,然后去找另外两个数?这就是优化的起点。

2.2 借助哈希表优化到两重循环

固定一个数 i,问题变成在剩下的元素中找两数之和等于target - nums[i]。这里有两个思路:一个是两重循环加哈希表,另一个是排序后双指针。

先用两重循环加哈希表举个例子:

def threeSum(nums): n = len(nums) res_set = set() for i in range(n): target = -nums[i] table = {} for j in range(i + 1, n): need = target - nums[j] if need in table: triplet = tuple(sorted([nums[i], nums[j], need])) res_set.add(triplet) table[nums[j]] = j return [list(t) for t in res_set]

这是把“两数之和”那张哈希表搬过来用。时间复杂度降到 O(n²),但有两个问题:

  • 结果去重还得靠最后排序 + set,因为同一个三元组可能被不同位置的 i 枚举到。
  • 空间复杂度 O(n),因为每次循环都建一张哈希表。

哈希表方案其实是可行的,但面试官大概率会让你继续优化,因为它在“去重”这件事上处理得很笨重,而且如果数组是排好序的,你还有更优雅的做法。

2.3 排序加双指针的经典解法

双指针解法的思路是:

  1. 先将数组排序。
  2. 固定最左边的下标 i,作为第一个数。
  3. 然后用两个指针 left 和 right 分别指向 i 后面区间的两端。
  4. 计算三数之和s = nums[i] + nums[left] + nums[right]。
  5. 如果s == 0,记录结果,然后移动左右指针并去重。
  6. 如果s < 0,说明左侧数太小,left += 1。
  7. 如果s > 0,说明右侧数太大,right -= 1。

为什么排序有用?因为排序后数组有序,两个指针才能根据和的大小进行“向左或向右”的调节。想象你有一根升序排列的刻度尺,一个指针从头走,一个指针从尾走,如果和太小,就把左指针右移放大;如果和太大,就把右指针左移缩小。这个过程非常像二分查找的“折中”思想,但它是同时调节两个端点。

这里有个关键推论:当第一个数固定时,剩余部分找两数之和等于固定值的问题,就能用双指针在线性时间内解决。整体复杂度就变成了最外层 O(n) 乘以内层双指针 O(n),总 O(n²)。

去重具体怎么写,是这项技术最大的坑,我单独用一节来讲。

3. 实操细节:去重、边界与代码实现

3.1 排序数组与指针移动的边界

先说一下排序后的边界。外层循环从i=0遍历到len(nums)-3就够了,因为至少还要留两个位置给 left 和 right。当然,你写成range(n)也不会越界,只要内部判断提前退出就行。但更严谨的写法是:

for i in range(len(nums) - 2): # ...

为什么是len(nums) - 2?因为 i 是三元组的最小元素(在有序数组里),如果 i 已经走到了倒数第三个元素之后,后面没有足够的位置放 left 和 right 了。

还有一个极其重要的剪枝:

if nums[i] > 0: break

因为数组已经有序,如果最小的 nums[i] 已经大于 0,那么后面任何两个数都比它大,三数之和必然大于 0,直接退出循环。同样,if nums[i] + nums[left] + nums[right] < 0就左指针右移,注意左指针增加后要继续跳过重复值。

指针移动时的边界条件要特别注意:while left < right这个条件必须始终成立,否则左右指针可能交叉。一旦交叉,说明区间内已经没有元素可用了,必须停止内层循环。在记录合法结果后,左右指针都需要移动,但移动时一定要跳过所有与当前值重复的元素,否则会产生相同三元组。

3.2 去重的完整逻辑

去重是整个题目的灵魂。很多网上的题解喜欢把去重写在两个地方:外层 i 的去重,以及内层 left/right 的去重。

先看外层 i 的去重:

if i > 0 and nums[i] == nums[i-1]: continue

这行代码的含义是:如果当前这个数的值和上一个数相同,那么当前这轮循环得到的所有三元组,在上一轮一定已经全部被找过了。为什么这么肯定?因为排序后,相同值一定连续出现。固定 nums[i-1] 时,它在数组后面的区间中找两个数与它匹配;固定 nums[i] 时,剩下的区间比上一轮更小,但之前的组合已经包含了所有可能,所以直接跳过是最安全、最优的写法。

有人会问:如果写成if i < n-1 and nums[i] == nums[i+1]: continue行不行?不行。因为你提前跳过了当前值,但当前值作为三元组的第一个数,它可能和后面的数组合出新的结果。举例:[-1,-1,0,1],i=0 的-1和 i=1 的-1值相同。如果看到“下一个元素相同”就跳过 i=1,那 i=1 这个位置就永远作为首元素参与不了外部循环,但事实上它自己并不需要作为首元素,因为 i=0 已经处理过所有包含一个-1的三元组,包括[-1,0,1]。这个例子里跳过 i=1 看起来没问题,但换成更复杂的情况,skip next会导致漏解。所以正确的观念就是:每个值第一次出现时,必须完整处理一轮;之后遇到相同值,直接跳过。

再看向内层 left/right 的去重。当找到一个三元组满足条件后,写法是:

ans.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

为什么要这两个 while?因为如果不跳过,比如[-2,0,0,2,2]中,i=0 固定-2,left 在第一个 0,right 在最后一个 2,记录[-2,0,2]。然后如果不跳过,left 右移一位还是 0,right 左移一位还是 2,又会记录相同的三元组,产生重复。所以必须在记录后,将 left 和 right 移动到与当前值不同的新位置,再分别往中间走一步。

这里有个容易犯的错误:只在 while 里跳过,但跳过之后忘了left += 1和right -= 1,导致指针还是指向重复元素。务必记住,while 循环跳的是“连续重复区间”,跳完后还需要正常移动一步。

另一个常见疑问:在s == 0之前,也就是指针还没找到正确答案时,要不要去重?不需要。因为只要s != 0,左右指针的移动方向是确定的,不会产生重复结果。重复只会从“记录成功”的那一刻开始出现。

3.3 Python 实现与核心注释

直接给出可运行的标准解法。

def threeSum(nums): nums.sort() n = len(nums) res = [] for i in range(n - 2): # 剪枝:第一个数大于0,后面的数更大,不可能有解 if nums[i] > 0: break # 去重:外层相同值只处理一次 if i > 0 and nums[i] == nums[i - 1]: continue left, right = i + 1, n - 1 while left < right: s = nums[i] + nums[left] + nums[right] if s < 0: left += 1 elif s > 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.sort()属于就地排序,会改变原数组。面试时如果面试官要求不能修改原数组,可以先用sorted(nums)复制一份再排序,代价是 O(n) 的空间。多数情况下,LeetCode 允许原地排序,所以直接用没问题。

关于 Python 的语法,注意nums[left] == nums[left + 1]中left + 1不要越界,但 while 条件里已经先做了left < right判断,所以 safe。同理nums[right - 1]也有left < right保护。

让我们手动跑一遍示例:nums = [-1,0,1,2,-1,-4],排序后变成[-4,-1,-1,0,1,2]。

  • i=0,nums[0] = -4。left=1,right=5,三数和为 -4 + (-1) + 2 = -3 < 0,left=2。再算 -4 + (-1) + 2 = -3,left=3。算 -4+0+2=-2,left=4;-4+1+2=-1;left=5 后结束。
  • i=1,nums[1] = -1。由于 i>0 且 nums[1]==nums[0],跳过。
  • i=2,nums[2] = -1。同样跳过。
  • i=3,nums[3]=0。left=4,right=5,和为0+1+2=3 >0,right=4,循环结束。
  • i=4,n-2? i range(4) 实际到3结束,所以 i=4 不会执行。

注意到最终结果里没有包含[-4,2,2]?不存在。结果是[[-1,-1,2],[-1,0,1]]。验证正确。

4. 易错点与常见问题排查

4.1 典型错误一:结果包含重复三元组

这个错误几乎每个人都犯过。比如写成这样:

if s == 0: res.append([nums[i], nums[left], nums[right]]) left += 1 right -= 1

而不做任何去重。对于输入[-2,0,0,2,2],i=0 固定 -2,left 指向 index1 的 0,right 指向 index4 的 2,记录[-2,0,2];然后 left+1 指向 index2 的 0,right-1 指向 index3 的 2,再次满足和为 0,又记录[-2,0,2]。最终返回[[-2,0,2],[-2,0,2]],这就是重复。

修复方法就是前面说的:在记录后,用两个 while 把 left 和 right 移动到无重复区域。另外,外层 i 的去重也不能少,否则当数组有多个相同首元素时也会重复。

4.2 典型错误二:指针越过边界

有人会在while left < right内部不小心写错指针移动顺序,比如在去重 while 之后再移动时,没重新检查left < right。严格来说,只要你的去重 while 都带了left < right条件,随后移动 one step 其实也有越界风险?比如 left 已经到 n-1 了,再去执行 left += 1 就变成 n。不过下一轮 while 判断会退出,不会访问越界元素,所以不会产生运行时错误。但如果你在去重 while 之后继续使用nums[left],就可能 IndexError。

还有人在外层循环中写for i in range(n)而不是range(n-2),然后在循环里left, right = i+1, n-1,当 i=n-2 或 n-1 时,虽然可能因为left >= right直接跳过,但容易让人困惑。更稳妥的做法是写成range(n-2),明确表示 i 最多到倒数第三个位置。

4.3 性能对比总结

为了让你直观感受复杂度差异,我列个表格:

方法时间复杂度空间复杂度适用规模
暴力三重循环O(n³)O(1) 或 O(n)(取决于去重方式)n < 100
哈希表辅助O(n²)O(n)n < 1000
排序 + 双指针O(n log n + n²) = O(n²)O(log n) 到 O(n)(取决于排序空间)n < 5000 没问题

注意排序本身是 O(n log n),但 n² 项主导,所以整体是 O(n²)。双指针方案的空间复杂度主要来自排序,Python 的 Timsort 通常需要 O(n) 额外空间,但多数面试讨论中我们只看额外辅助空间,如果不算排序,双指针本身只用 O(1) 空间。这也是它优于哈希表方案的一个点。

实测 LeetCode 上,nums长度达到 3000 时,双指针解法用时约几十毫秒,哈希表方案可能慢一两倍,而且代码更难去重。所以面试时推荐直接写双指针。

5. 扩展思考与个人经验

5.1 变形题:最接近的三数之和

LeetCode 16 题“最接近的三数之和”几乎是同一套代码,只改一点点。题目要求返回与target最接近的三数之和。解法还是排序加双指针,但在每次计算s时,用一个变量记录diff = abs(s - target),如果diff小于当前最小差,就更新答案。然后根据s与target的大小关系移动指针。去重逻辑甚至可以省略,因为只返回一个数,不要求列出所有组合。

如果你能熟练写出三数之和的骨架,16 题就等于白送分。我建议你写三数之和时,顺便用多一个参数去实现“小于等于 target 的最大和”之类的功能,这样面试变形题也能游刃有余。

5.2 环形链式思路:nSum 原则

三数之和再往上扩展,就是四数之和、五数之和。LeetCode 18 题“四数之和”要求固定两个数,剩下两数用双指针。代码逻辑是在三数之和的外面再套一层循环:

for i in range(n - 3): if nums[i] > target and target > 0: break 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 # 双指针逻辑

你会发现,nSum 问题的套路就是:递归或者嵌套循环固定前 n-2 个元素,最后两个元素用双指针找。这就是很多算法模板里的 nSum 思想。理解了三数之和,四数之后只是重复劳动。

我在实际刷题时总结了一个小技巧:写 nSum 时,把递归的参数设计成sums(nums, target, count, start, path),用递归逼近 base casecount == 2,然后双指针查两数之和。这种方式虽然代码量多一点,但可以一次性解决 2Sum、3Sum、4Sum,甚至多到 5Sum。不过面试时不要一上来就写递归,先用最直接的迭代更不容易出错。

5.3 面试时如何分析和作答

面试官如果让你做三数之和,一定要先说思路,而不是直接沉默着写代码。我建议你按下面这个模板组织语言:

第一,确定暴力解:三重循环枚举,O(n³),然后说能不能优化。第二,观察到如果数组有序,固定一个数后,剩余两数可以用双指针,因为有序性保证了移动指针的正确性。第三,说明复杂度降到 O(n²),同时详细介绍去重策略。第四,手写代码时,注意剪枝条件nums[i] > 0和越界判断。

面试官常会追问几个问题,比如“为什么排序后不会丢解?”你要解释:排序只改变元素顺序,不改变组合的存在性;双指针枚举的是所有 i、left、right 的有效组合,不会漏掉任意一个和为 0 的三元组。再比如“如果数组中有很多重复元素,会不会有性能问题?”你要答:重复元素通过 while 跳过,每个重复元素只会被指针扫过一次,整体依然 O(n²),不会退化。

我个人在实际操作中还发现,很多人手写代码时把right的初始值写成n而不是n-1,导致越界。这种低级错误非常冤枉。只要记住:数组索引从 0 开始,最后一个元素是n-1,内层循环才能 run。

另外想提醒一点:题解帖子里的“去重”代码五花八门,有些写法在理论上对,但可读性差。比如有人写成while left < right and nums[left] == nums[left+1]: left+=1之后没有额外left+=1,而是把 while 的条件改成nums[left] == nums[left-1],看着很绕。我建议你固定一个模式:先跳过重复,再统一 +1,这样不容易乱。

说到底,三数之和值得你反复手写三次:第一次照着题解抄,第二次合上书自己写,第三次限时 15 分钟从零开始。三次之后,这类双指针问题基本就能形成肌肉记忆了。

最后分享一个刷题习惯:每写完一道题,我会在代码注释里补充一行“这题犯了什么错”。比如三数之和这一题,我当时的注释是“去重必须在记录后,不能在记录前”。等你刷到 100 题时,回头看这些注释,就是最宝贵的错题本。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/25 20:40:26

高校AI论文检测终极解密!为什么纯手写也会被判AI?

每年毕业季&#xff0c;都有一大批同学无比困惑&#xff1a;整篇论文纯手工一字一句写、没有复制粘贴、没用AI生成&#xff0c;最后AI检测却风险偏高&#xff0c;面临退回重写、延迟答辩、盲审扣分。 反之有些同学借助工具辅助优化&#xff0c;却能稳稳过审。核心原因是&#…

作者头像 李华
网站建设 2026/10/18 19:04:00

从自然语言到三维模型:Text-to-CAD核心链路与工程实践

从我第一次看到 text-to-cad 这个方向的时候&#xff0c;我就知道这不是一个“锦上添花”的玩具功能。做机械设计、结构件或者非标自动化的人&#xff0c;应该都有过那种体验&#xff1a;产品经理丢过来一句话&#xff0c;“这里加一个带四个安装孔的底板&#xff0c;孔距 50&a…

作者头像 李华
网站建设 2026/10/18 19:04:00

SpringBoot养老中心管理系统实战:从表设计到权限定时任务部署

上个月我陪家里人去看望一位住在养老中心的长辈&#xff0c;前台小姑娘手忙脚乱地翻纸质台账找探访登记信息&#xff0c;旁边的护理员拿着手机在群里发交接班记录。那一刻我突然意识到&#xff0c;这就是“养老中心管理系统”要解决的问题——不只是把Excel搬到网页上&#xff…

作者头像 李华
网站建设 2026/10/18 19:03:59

国自然中标后如何做好项目管理:经费、进展报告与结题避坑指南

说起“国自然中标者”&#xff0c;身边人的第一反应往往是恭喜、请客、上岸。但真正拿到资助结果通知的那一瞬间&#xff0c;我见过太多人反而沉默了&#xff1a;下一站怎么走&#xff1f;计划书要不要改&#xff1f;经费怎么花才能不踩红线&#xff1f;年度进展报告到底要写到…

作者头像 李华
网站建设 2026/10/25 20:38:25

AI Agent生产落地实战:可执行、可调试、可交付的工作流设计

1. 这不是概念炒作&#xff0c;是我在真实项目里反复拆解、重装、推翻重来的血泪笔记“AI Agent 到底是什么&#xff1f;”——这句话我去年在 Slack 群里问过 7 次&#xff0c;每次得到的答案都不一样&#xff1a;有人说是“带记忆的 ChatGPT”&#xff0c;有人说是“能自动点…

作者头像 李华
网站建设 2026/10/25 3:23:07

Unity GLTF加载实战:Runtime动态加载与HDRP/URP兼容方案

简介&#xff1a;本资源是面向Unity开发者的一站式GLTF模型支持插件包&#xff0c;专为解决Unity原生对GLTF格式支持有限的问题而设计&#xff0c;适用于游戏开发、Web3D可视化及轻量级AR/VR项目中的高效模型导入与实时渲染需求。压缩包共162个文件&#xff0c;包含39个C#脚本&…

作者头像 李华