面试必考!三数之和:双指针如何把 O(n³) 砍成 O(n²)?
2026/8/24 3:38:02 网站建设 项目流程

LeetCode 15「三数之和」,「面试命中率 TOP 3」,但也是很多人的“噩梦”:

  • 暴力三重循环 → 超时
  • 用 HashSet 去重 → 复杂得想摔键盘
  • 边界条件一多 → 代码直接崩

「但如果你掌握了双指针,这道题就是送分题。」

这是一套「排序 + 对撞指针 + 三重去重」的固定套路,以后遇到“K数之和”都能秒杀。


📦 题目速览(30秒读懂)

给你一个数组nums,找出所有「不重复」的三元组[a,b,c],使得a+b+c=0

「示例:」

输入:[-1,0,1,2,-1,-4] 输出:[[-1,-1,2], [-1,0,1]]

「约束:」长度 3~3000,数值 ±1e5 —— 暴力必死。


🧠 核心思路:从“三重循环”到“一重循环+双指针”

暴力在哪儿?

foriinrange(n):
forjinrange(i+1,n):
forkinrange(j+1,n):
ifnums[i]+nums[j]+nums[k]==0: ...

O(n³) = 270亿次(n=3000),直接超时 + 去重地狱。

优化脑回路

固定一个数nums[i],问题退化成:「在剩余数组中找两个数,使其和 = -nums[i]」
这就是经典的“两数之和”,而「有序数组」上的两数之和可以用「对撞双指针」O(n) 解决。

「于是三步走:」

  1. 「排序」(升序)—— 让数组有序,双指针才有意义。
  2. 「固定 i」,用leftright从两端往中间走。
  3. 「根据三数之和与 0 的大小」,决定移动左/右指针。

🖼️ 图解全过程(手把手带你走一遍)

nums = [-1,0,1,2,-1,-4]→ 排序后[-4, -1, -1, 0, 1, 2]

第一轮:固定 i=0,nums[i]=-4,目标 target=4

leftrightsum比较动作
1(-1)5(2)1< 4left++
2(-1)5(2)1< 4left++
3(0)5(2)2< 4left++
4(1)5(2)3< 4left++

→ left≥right 结束,无结果。

第二轮:固定 i=1,nums[i]=-1,target=1

leftrightsum比较动作
2(-1)5(2)1== 1✅ 记录 [-1,-1,2],left++, right--
3(0)4(1)1== 1✅ 记录 [-1,0,1],left++, right--

→ 结束,找到两组。

第三轮:i=2,nums[2]=-1 与上一轮相同 →「跳过(去重)」

后续 i=3,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,三数和不可能为0
ifnums[i] >0:
break
# 外层去重:跳过重复的 i
ifi >0andnums[i] == nums[i-1]:
continue

left, right = i +1, n -1
whileleft < right:
total = nums[i] + nums[left] + nums[right]
iftotal <0:
left +=1
eliftotal >0:
right -=1
else:
res.append([nums[i], nums[left], nums[right]])
# 内层去重:跳过左/右重复元素
whileleft < rightandnums[left] == nums[left+1]:
left +=1
whileleft < rightandnums[right] == nums[right-1]:
right -=1
# 同时收缩
left +=1
right -=1
returnres

Java 版

classSolution{
publicList<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> 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[left+1]) 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) 数之和,直到 K=2 用双指针

💬 面试追问模拟(提前准备,惊艳全场)

「Q1:为什么一定要先排序?不排序能双指针吗?」

不能。对撞双指针依赖“单调性”——有序时,我们才敢根据sumtarget的大小决定左移还是右移。无序数组上移动哪个指针无法保证正确性。

「Q2:如果数组全正数或全负数,可以提前结束吗?」

可以。排序后若nums[i] > 0,说明最小的数都大于 0,三正数不可能和为 0,直接break。这就是代码中的剪枝。

「Q3:nums[i] == nums[i-1]去重时,为什么不用nums[i] == nums[i+1]?」

因为i作为固定数,如果和前一个相同,那么当前这一轮产生的三元组必然被前一轮包含。而如果用i+1,会误把本应使用的相同元素跳过(比如[-1,-1,2]中的两个 -1),导致漏解。


🧩 实战小技巧(刷题党必备)

  • 「固定模板」:凡是“K数之和”类题目,一律先排序,再递归/循环降维,最后用双指针收尾。
  • 「去重口诀」:外层跳i,内层跳leftright——「跳左不跳右,跳右不跳左,两边都跳完再收缩」
  • 「边界条件」:数组长度 < 3 直接返回空;nums[i] > 0直接 break(因为已经排序)。

📈 实际应用场景(让知识落地)

  • 「金融风控」:在有序的交易记录中,快速找到三笔金额之和为 0 的异常交易组合。
  • 「推荐系统」:用户兴趣向量排序后,双指针找出兴趣互补的用户对。
  • 「数据清洗」:在已排序的日志时间戳中,快速匹配三段时间之和满足特定条件的异常点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询