ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

三数之和模板:排序+双指针+去重,面试手撕一次写对

三数之和模板:排序+双指针+去重,面试手撕一次写对 说个反直觉的现象一堆人看三数之和的题解时觉得自己已经完全懂了合上手机白板手写十分钟写出三行bug去掉重写漏、该break没break、指针越界查半天。这道题在算法面试里属于典型的T1级别高频手撕题LeetCode 15题字节、腾讯、阿里、美团这些公司的题库里都拿它当过热手题面试官用它考察的点根本不是你会不会解而是你在紧张状态下能不能十到十五分钟内写出边界完全正确、还能把思路讲清楚的代码。这篇内容就是给你一套可以直接背下来的解法模板外加每一行的记忆点、三处去重的完整拆解、面试追问的应对方式以及我实际练题和带人刷题时踩过的坑。适合准备校招社招算法面的人也适合刚学双指针想找一道代表性题目吃透的读者。1. 为什么三数之和值得花力气背成模板1.1 面试定位热手题和压力题的合体三数之和这道题考察频率在LeetCode所有题目里能排进前几。它的难度标注是Medium但实际面试中的杀伤力远超很多Hard题。原因在于它逻辑上不难但细节极其密集。你需要在十分钟里同时处理排序、双指针、外层去重、内层去重、剪枝、边界条件任何一个环节出错都会导致答案错误或者超时。面试官往往把这道题安排在面试前半段作为热手题让你进入状态同时也作为压力题观察你在短时间内的工程判断力。我见过不少候选人一上来就说我知道这题用双指针但写到一半开始纠结去重逻辑最后不得不涂改重写。这类表现对面试评价的杀伤力很大——不是算法能力问题而是稳定性问题。1.2 可直接背不等于死记硬背很多刷题博主会告诉你理解最重要不要背题。这话对一半。对于三数之和这种范式型题目你需要做的是把排序双指针这个组合套路刻进肌肉记忆达到闭眼默写的程度。面试时你的认知资源有限如果连模板都要现场推导根本没有余力去和面试官讨论优化、变体和边界case。背模板的另一个好处是三数之和是四数之和、最接近的三数之和、三数之和小于K等一大票变体题的母板。模板熟了变体题就是改参数的事。所以这篇标题说可直接背本质是让你先拥有一份标准可靠的实现再在它的基础上做增量理解。1.3 这道题到底在考察什么我把面试官想看的能力点列一下是否能主动想到排序并说清楚排序带来的好处。是否理解双指针收敛的本质从而不会出现指针回退的错误。是否能把重复分支合并写出结构清晰的代码。是否能处理空数组、长度不足3、全正数等边界case。是否能在写完代码后主动测试样例并解释去重的正确性。这些能力点不是孤立的它们恰好全部落在三数之和这一道题上。所以这道题才配得上T1级别高频手撕题的称号。2. 背之前先把原理吃透排序加双指针为什么能成立2.1 暴力解法为什么注定过不了先看最直接的思路三重循环枚举所有 ijk判断 nums[i] nums[j] nums[k] 0。这个方案的代码非常短但时间复杂度是O(n^3)。当n取3000时组合数大约是C(3000, 3)接近45亿次哪怕按每秒1亿次运算来算也要45秒才能跑完面试场景下直接不成立。关键不是记住O(n^3)很慢这个结论而是理解优化方向如何省掉一层循环双指针方案就是在固定一个数之后把剩下的两数之和问题从O(n^2)降到O(n)从而让总复杂度变成O(n^2)。2.2 排序带来的三个关键红利很多人不理解为什么三数之和要先排序。排序乍一看引入了O(n log n)的开销但它为后面省掉了更多工作一是相同的元素聚在一起。数组排完序后重复值必然是相邻的。去重只需要比较nums[i]和nums[i-1]、nums[left]和nums[left1]即可不需要借助哈希集合。这是让代码简洁并可控的最重要原因。二是双指针可以收敛。在有序数组中固定住最小的数nums[i]之后问题变成在 i 右侧的区间里找两个数使它们的和等于 -nums[i]。此时如果nums[left] nums[right]偏小说明左指针对应的值太小、需要向右移动如果偏大说明右指针太大了、需要向左移动。因为数组单调这种单向移动一定是有效的。三是可以剪枝。排序后如果外层循环枚举到的nums[i]已经大于0那它右侧所有数都大于0三数之和必然大于0直接break不需要继续遍历。这三个红利不是孤立的它们共同决定了排序方案是面试中的最优解。2.3 双指针为什么不会漏解这是面试官最爱追问的一点。你可以用排除法来解释在区间[left, right]里寻找和为target的两个数。每次比较当前和若 nums[left]nums[right] target则右指针是当前区间最大值连用最大右指针都没法让和达到target说明左打印机和任意比右指针更小的数组合都不成立。因此所有包含当前left的方案都被排除left可以放心右移。若 nums[left]nums[right] target则左指针是当前区间最小值连用最小左指针都没法让和降到target说明右指针和任意比左指针更大的数组合都不成立。因此所有包含当前right的方案都被排除right可以放心左移。若相等则记录答案并把两个指针同时移动继续搜索。这里的核心是每次移动都排除了一整类不可能的解所以不会漏解。用一句通俗的话讲双指针不是暴力尝试所有组合而是每次根据大小关系把不可能的那半边直接扔掉。2.4 三个分支怎么统一记忆内层双指针一共只有三种情况我建议你就按这个顺序写和等于目标值记录三元组然后两边同时缩并且跳过所有重复值。和小于目标值说明值太小把左指针往右挪。和大于目标值说明值太大把右指针往左挪。三种情况互斥用if/elif/else写不需要多余的判断。很多人在现场会写出奇怪的嵌套if多半是没想清楚这三种情况天然就是一个完整的分支结构。3. 可直接背的 Python 模板与每一行的记忆点3.1 主模板代码下面这份代码是我在面试中实际用过的版本也是我推荐你背的这一版。它把剪枝、去重和双指针组织的比较干净def threeSum(self, nums: List[int]) - List[List[int]]: nums.sort() n len(nums) res [] for i in range(n - 2): # 剪枝最小的数已经大于0后面不可能凑出和为0 if nums[i] 0: break # 外层去重跳过作为第一个数的重复值 if i 0 and nums[i] nums[i - 1]: continue left i 1 right n - 1 target -nums[i] while left right: s nums[left] nums[right] if s target: 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和right都停在非重复的最后一个位置同时收缩 left 1 right - 1 elif s target: left 1 else: right - 1 return res3.2 记忆链条按步骤默写你不需要逐行背你需要背的是下面这条链条。默写时按链条展开即可排序 → 取长度n → 初始化结果数组 → 外层for i in range(n - 2) → 剪枝break → 外层去重continue → 初始化left、right、target → 内层while left right → 计算两数之和s → 三种分支 → 找到解后先跳过重复再同时收缩 → 返回结果。这套链条的每一个环节都可以对应一段固定代码面试时你在白板上先写出骨架再一步步细化逻辑就不会乱。注意外层循环的边界写成range(n - 2)这是为了保证至少有i、left、right三个位置写成range(n)虽然内层会在leftright处兜底但变量边界会变脏别给自己留隐患。3.3 C版本同样逻辑换一层皮如果你面试用的是C模板同样固定class Solution { public: vectorvectorint threeSum(vectorint nums) { sort(nums.begin(), nums.end()); vectorvectorint res; int n nums.size(); for (int i 0; i n - 2; i) { if (nums[i] 0) break; if (i 0 nums[i] nums[i - 1]) continue; int left i 1, right n - 1; int target -nums[i]; while (left right) { int s nums[left] nums[right]; if (s target) { res.push_back({nums[i], nums[left], nums[right]}); while (left right nums[left] nums[left 1]) left; while (left right nums[right] nums[right - 1]) right--; left; right--; } else if (s target) { left; } else { right--; } } } return res; } };Java版本就不贴了代码几乎和C一致只是容器换成List。你只需要记住一个语言版本的核心逻辑其他语言只是语法翻译。3.4 一个偷懒但管用的默写技巧默写时最容易漏的是找到解后的双端跳过重复。我有两个办法防止漏写第一把这一整段当成一个整体记忆不要拆开记。第二给自己定一个固定话术写代码时嘴里默念记录、跳左重、跳右重、双缩。写完之后马上再检查一遍这一段有没有保留left right条件有没有同时left和right--这两行检查做完基本不会错。4. 最容易写错的三处去重逻辑全部拆开讲4.1 外层i的去重为什么比较i和i-1而不是i和i1很多初学者写外层去重时会写成# 错误示范 if nums[i] nums[i 1]: continue这个写法看起来很自然实际是错的。原因在于当i指向一个重复值中的第一个时我们仍然需要用它作为第一个数去和后面的组合只有当i指向重复值中的第二个及以后时才应该跳过。比较nums[i]和nums[i 1]会把第一个重复值也跳过。举个例子数组[-1, -1, 2]的正确答案是[-1, -1, 2]。按错误写法i0时发现nums[0]nums[1]直接continue整个答案都丢了。所以正确写法一定是比较当前值和前一个值if i 0 and nums[i] nums[i - 1]。这个细节我建议你反复练习因为它是面试中最高频的bug之一。4.2 内层找到一组解后为什么左右都要跳当s target时我们得到了一组解。此时如果只移动left或者只移动right会发生什么比如数组[-1, 0, 1, 2, -1, -4]排序后是[-4, -1, -1, 0, 1, 2]不加内层去重时i指向第一个-1双指针会找到[-1, 0, 1]然后i指向第二个-1又会找到[-1, 0, 1]结果数组里出现两份相同答案。所以必须在找到一组解后把内层所有与当前left、right重复的值跳过。为什么左右要同时收缩因为left和right这两个位置已经合作完成了一个合法组合固定i的情况下left取这个值时能配对得到target的right是唯一的同样right取这个值时能配对得到target的left也是唯一的。这个组合已经被记录过了左右两端再任何一方停留在当前值都不会产生新的合法三元组所以必须双向移动同时通过跳过重复值来避免生成完全相同的结果。注意跳过去重这个动作本身的语法找到解后左指针要跳的是和nums[left]相等的值也就是while left right and nums[left] nums[left 1]: left 1右指针要跳的是和nums[right]相等的值也就是while left right and nums[right] nums[right - 1]: right - 1。跳完之后left和right分别停在最后一个重复值上再统一执行left和right--落到新的非重复位置。这个先跳到最后一个重复值再统一收缩的顺序比先收缩再跳更不容易写错。4.3 为什么内层while里时刻都要带left right所有涉及left或right移动的位置都必须保证left right才合法。特别是去重循环里你可能会想写# 错误示范可能越界 while nums[left] nums[left 1]: left 1当left撞上right之后nums[left 1]就可能访问到数组末尾越界或者把一个不合法的值算进来。所以我的习惯是凡是在循环内部出现了nums[left 1]、nums[right - 1]这类预测下一个位置的代码一律先检查前一个条件。写多了之后这个检查会变成肌肉记忆。4.4 用标准样例验证去重逻辑背完模板后建议你自己手动跑两个用例第一个是题目的标准样例nums [-1,0,1,2,-1,-4]预期输出是[[-1,-1,2],[-1,0,1]]。跑的过程里注意观察i0nums[0]-4找不到和为4的两数组合i1nums[1]-1内层找到[-1,0,1]i2nums[2]-1被外层去重跳过。这样输出的两个三元组分别是[-1,-1,2]和[-1,0,1]。第二个是nums [-1, -1, 2]预期输出是[[-1,-1,2]]。这个用例专门用来检验外层去重是不是写成了nums[i]nums[i1]。如果按错误写法输出会是[]一眼就能暴露问题。这两个用例加起来用不了两分钟却能挡住面试中最常见的两类错误我强烈建议你把它当成模板的一部分一起背。5. 时间复杂度和空间复杂度的标准回答5.1 时间复杂度O(n log n) O(n^2)分两部分算排序基于比较的排序比如Python的Timsort和C的std::sort时间复杂度O(n log n)。外层循环加内层双指针外层i要遍历n-2次内层while每次从left和right两头往中间收每一轮最多移动O(n)步所以内层整体是O(n)外层嵌套后总成本O(n^2)。综合起来整体时间复杂度是O(n^2)。面试时不要只说O(n^2)就停建议把排序的O(n log n)单独提一下表示你没有忽略排序这一步。如果数组长度很大排序的开销占比会越来越小最终由O(n^2)主导。5.2 空间复杂度看你怎么看待排序的辅助空间如果不把返回结果res算进去额外空间主要消耗在排序上。Python的Timsort平均需要O(n)的辅助空间C的std::sort是原地排序通常只使用O(log n)的递归栈空间最坏情况下可以是O(n)。所以标准回答可以说额外空间O(log n)到O(n)取决于语言和排序实现。有一个容易踩的误区是面试官问空间复杂度时你把res数组算进去了。res是题目要求返回的结果属于输出空间一般不计入额外空间复杂度。如果面试官追问这一点你可以明确说如果不考虑返回结果占用的空间。5.3 为什么整体比暴力法快了一个数量级这个说清楚能体现出你对复杂度的理解。暴力法是三重循环每一次枚举都要判断双指针方案通过排序把内层变成了一个线性收缩的过程在固定外层i的前提下内层指针一共只从两端到中间走一次同层不会嵌套第二层循环因此少了一维复杂度。记住这个逻辑面试官怎么追问你都能接住。6. 面试追问与变体题怎么把模板改造成四道题6.1 目标值不是0怎么办面试官会问如果target不是0而是任意整数怎么改非常简单把外层循环里的target从 -nums[i] 变成 target - nums[i] 就行。其余逻辑完全不动。这道变题考察的是你有没有真的理解模板而不是只会背原题。6.2 最接近的三数之和这是LeetCode第16题也是三数之和最常见的变体。模板可以复用区别是内层不再是等于target就记录而是每次计算当前和与target的差值的绝对值如果更小就更新答案。然后仍然根据和与target的大小关系移动指针。注意不需要去重因为题目只要求返回一个最接近的和不要求枚举组合。6.3 四数之和第18题思路是把四数之和降成三数之和。先固定两个数nums[i]和nums[j]剩下的问题变成在j右侧找两个数和等于target - nums[i] - nums[j]这正好是我们的内层双指针。整体复杂度变成O(n^3)。去重逻辑需要在外层两层分别处理但模式和三数之和完全一致一个模板吃遍三兄弟。6.4 面试官要你返回下标怎么办这个问题很阴险因为排序方案会破坏原数组的下标关系。你不能无脑排序。一般有两种思路一是用哈希表存值 - 下标列表两数之和那套方案做改造二是把数组值和原始下标打包成结构体再排序。不管选哪种都要先跟面试官确认返回的是所有组合的下标三元组去重还是任意一个。这个场景不太可能在三数之和里问得很深但提前知道总比现场懵好。6.5 如果只需要判断是否存在如果只要求回答是否存在三个数和为0那不需要返回所有组合可以去掉所有去重逻辑遇到第一组解就返回True。复杂度不变但代码更短。更多时候面试官不会这么简化因为题目核心就是考察去重。7. 我实际面试和带人刷题时踩过的几个坑7.1 命名混乱是手撕代码的第一杀手我看到太多人写这道题时用i、j、k来命名三个指针结果写到最后自己都分不清哪个是外层、哪个是内层。我的建议是固定命名外层循环变量用i内层左指针用left右指针用right。这样你在心里默念固定最左边的数left从右往中间走right从最右往中间走逻辑永远清晰。7.2 先处理空数组和长度小于3的情况虽然不处理也能通过大部分用例但面试官看一眼边界处理就知道你的工程习惯。在函数开头写上if not nums or len(nums) 3: return []这行代码永远不会错但能体现你考虑问题的全面性。注意不要把这个判断写在排序之前或之后搞混了顺序无关紧要关键是别漏掉。7.3 不要为了提升复杂度去答哈希解法有人会提到用哈希表存两数之和来过三数之和理论上可以做到平均O(n^2)但去重处理极其痛苦面试官听你讲哈希去重的过程会觉得你在绕圈子。标准答案就是排序加双指针简洁、正确、可证明。哪怕你知不知道哈希解法都不建议作为主答案。7.4 每天十分钟的肌肉记忆训练我的经验是这道题值得连续一周每天闭眼默写一遍。每次写完全程大约五分钟写错了就对照模板找出差异。坚持下来后你会发现面试时根本不会紧张因为你写的每一步都像打字一样自然。这种题目最怕的不是不会而是明明会却写错多练几遍比多看十篇题解有用。7.5 最后分享一个临场小技巧写代码前先用一句话把思路告诉面试官我先排序然后固定第一个数剩下两个数用双指针在右侧区间找。每找到一个解左右指针同时移动并跳过重复值。这句话说完面试官已经知道你真的懂了接下来你写代码时即使偶尔卡壳面试官也更容易帮你而不是觉得你不行。三数之和这道题几个月前我准备面试时最怕它练熟了之后反而最希望面试官考它——因为它是少有的背模板就能稳稳拿分的手撕题。你现在花十几分钟把上面的逻辑和代码过一遍再用一周每天默写一遍下次在面试里遇到它你会比大多数候选人从容得多。
返回列表