面试必考!三数之和:双指针如何把 O(n³) 砍成 O(n²)?

📅 2026/8/24 3:38:03
面试必考!三数之和:双指针如何把 O(n³) 砍成 O(n²)?
LeetCode 15「三数之和」「面试命中率 TOP 3」但也是很多人的“噩梦”暴力三重循环 → 超时用 HashSet 去重 → 复杂得想摔键盘边界条件一多 → 代码直接崩「但如果你掌握了双指针这道题就是送分题。」这是一套「排序 对撞指针 三重去重」的固定套路以后遇到“K数之和”都能秒杀。 题目速览30秒读懂给你一个数组nums找出所有「不重复」的三元组[a,b,c]使得abc0。「示例」输入[-1,0,1,2,-1,-4] 输出[[-1,-1,2], [-1,0,1]]「约束」长度 3~3000数值 ±1e5 —— 暴力必死。 核心思路从“三重循环”到“一重循环双指针”暴力在哪儿foriinrange(n):forjinrange(i1,n):forkinrange(j1,n):ifnums[i]nums[j]nums[k]0: ...O(n³) 270亿次n3000直接超时 去重地狱。优化脑回路固定一个数nums[i]问题退化成「在剩余数组中找两个数使其和 -nums[i]」。这就是经典的“两数之和”而「有序数组」上的两数之和可以用「对撞双指针」O(n) 解决。「于是三步走」「排序」升序—— 让数组有序双指针才有意义。「固定 i」用left和right从两端往中间走。「根据三数之和与 0 的大小」决定移动左/右指针。️ 图解全过程手把手带你走一遍以nums [-1,0,1,2,-1,-4]→ 排序后[-4, -1, -1, 0, 1, 2]第一轮固定 i0nums[i]-4目标 target4leftrightsum比较动作1(-1)5(2)1 4left2(-1)5(2)1 4left3(0)5(2)2 4left4(1)5(2)3 4left→ left≥right 结束无结果。第二轮固定 i1nums[i]-1target1leftrightsum比较动作2(-1)5(2)1 1✅ 记录 [-1,-1,2]left, right--3(0)4(1)1 1✅ 记录 [-1,0,1]left, right--→ 结束找到两组。第三轮i2nums[2]-1 与上一轮相同 →「跳过去重」后续 i3,4,5 因为剩余元素不足两个循环自然结束。「最终答案」[[-1,-1,2], [-1,0,1]]✅ 代码实现Python Java 双版本可直接运行Python 版带详细注释classSolution:defthreeSum(self, nums: List[int])- List[List[int]]:nums.sort()n len(nums)res []foriinrange(n -2):# 剪枝最小的数都 0三数和不可能为0ifnums[i] 0:break# 外层去重跳过重复的 iifi 0andnums[i] nums[i-1]:continueleft, right i 1, n -1whileleft right:total nums[i] nums[left] nums[right]iftotal 0:left 1eliftotal 0:right -1else:res.append([nums[i], nums[left], nums[right]])# 内层去重跳过左/右重复元素whileleft rightandnums[left] nums[left1]:left 1whileleft rightandnums[right] nums[right-1]:right -1# 同时收缩left 1right -1returnresJava 版classSolution{publicListListInteger threeSum(int[] nums) {Arrays.sort(nums);ListListInteger res newArrayList();intn nums.length;for(inti 0; i n -2; i) {if(nums[i] 0)break;if(i 0 nums[i] nums[i-1])continue;intleft i 1, right n -1;while(left right) {intsum nums[i] nums[left] nums[right];if(sum 0) left;elseif(sum 0) right--;else{res.add(Arrays.asList(nums[i], nums[left], nums[right]));while(left right nums[left] nums[left1]) left;while(left right nums[right] nums[right-1]) right--;left; right--;}}}returnres;}}⚠️「提醒」三处去重i、left、right缺一不可漏掉一个就会输出重复三元组面试直接扣分。⏱️ 复杂度分析面试必问「时间」排序 O(n log n) 外层循环 O(n) * 内层双指针 O(n) 「O(n²)」「空间」O(log n) 排序栈空间不计返回结果通常说「O(1)」额外空间。 举一反三面试官最爱问的 4 个变种题目差异点应对策略「LeetCode 18. 四数之和」找 4 个数外面再套一层循环内部还是双指针O(n³)「LeetCode 16. 最接近的三数之和」找和最接近 target记录最小差值移动逻辑不变「LeetCode 259. 较小的三数之和」统计和 target 的个数双指针找到后right - left批量计数「K 数之和通用」K 个数的和递归固定一个数降为 (K-1) 数之和直到 K2 用双指针 面试追问模拟提前准备惊艳全场「Q1为什么一定要先排序不排序能双指针吗」不能。对撞双指针依赖“单调性”——有序时我们才敢根据sum与target的大小决定左移还是右移。无序数组上移动哪个指针无法保证正确性。「Q2如果数组全正数或全负数可以提前结束吗」可以。排序后若nums[i] 0说明最小的数都大于 0三正数不可能和为 0直接break。这就是代码中的剪枝。「Q3nums[i] nums[i-1]去重时为什么不用nums[i] nums[i1]」因为i作为固定数如果和前一个相同那么当前这一轮产生的三元组必然被前一轮包含。而如果用i1会误把本应使用的相同元素跳过比如[-1,-1,2]中的两个 -1导致漏解。 实战小技巧刷题党必备「固定模板」凡是“K数之和”类题目一律先排序再递归/循环降维最后用双指针收尾。「去重口诀」外层跳i内层跳left和right——「跳左不跳右跳右不跳左两边都跳完再收缩」。「边界条件」数组长度 3 直接返回空nums[i] 0直接 break因为已经排序。 实际应用场景让知识落地「金融风控」在有序的交易记录中快速找到三笔金额之和为 0 的异常交易组合。「推荐系统」用户兴趣向量排序后双指针找出兴趣互补的用户对。「数据清洗」在已排序的日志时间戳中快速匹配三段时间之和满足特定条件的异常点。