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) 解决。
「于是三步走:」
「排序」(升序)—— 让数组有序,双指针才有意义。 「固定 i」,用 left和right从两端往中间走。「根据三数之和与 0 的大小」,决定移动左/右指针。
🖼️ 图解全过程(手把手带你走一遍)
以nums = [-1,0,1,2,-1,-4]→ 排序后[-4, -1, -1, 0, 1, 2]
第一轮:固定 i=0,nums[i]=-4,目标 target=4
| left | right | sum | 比较 | 动作 |
|---|---|---|---|---|
| 1(-1) | 5(2) | 1 | < 4 | left++ |
| 2(-1) | 5(2) | 1 | < 4 | left++ |
| 3(0) | 5(2) | 2 | < 4 | left++ |
| 4(1) | 5(2) | 3 | < 4 | left++ |
→ left≥right 结束,无结果。
第二轮:固定 i=1,nums[i]=-1,target=1
| left | right | sum | 比较 | 动作 |
|---|---|---|---|---|
| 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:为什么一定要先排序?不排序能双指针吗?」
不能。对撞双指针依赖“单调性”——有序时,我们才敢根据
sum与target的大小决定左移还是右移。无序数组上移动哪个指针无法保证正确性。
「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,内层跳left和right——「跳左不跳右,跳右不跳左,两边都跳完再收缩」。「边界条件」:数组长度 < 3 直接返回空; nums[i] > 0直接 break(因为已经排序)。
📈 实际应用场景(让知识落地)
「金融风控」:在有序的交易记录中,快速找到三笔金额之和为 0 的异常交易组合。 「推荐系统」:用户兴趣向量排序后,双指针找出兴趣互补的用户对。 「数据清洗」:在已排序的日志时间戳中,快速匹配三段时间之和满足特定条件的异常点。