三数之和(Three Sum)是一个非常经典、非常容易写错、也非常适合训练思维的题。
很多人第一次做这题的状态是:
- 思路能想出来
- 代码能跑
- 结果一堆重复答案,直接 GG
这篇笔记,我会围绕一个核心目标来讲:
为什么一定要排序 + 双指针?
去重到底在去什么?
一、题目回顾
给你一个整数数组 nums,判断是否存在三个元素 a, b, c,使得:
a + b + c = 0
返回所有不重复的三元组。
注意关键词:
不重复,这是这道题的灵魂。
二、为什么暴力解法不行?
最直观的写法是三层循环:
i + j + k == 0
问题有两个:
- 时间复杂度是 O(n³),数组稍微大一点就超时
- 完全没法优雅地去重
所以这道题的正确打开方式只有一条路:
先排序,再用双指针
三、排序的真正意义
排序不是为了好看,而是为了两件事:
- 让双指针成立
- 让去重变得可能
排序之后,数组满足:
nums[i] <= nums[left] <= nums[right]
这会带来一个非常重要的性质:
- 当前和小了,只能让 left 右移
- 当前和大了,只能让 right 左移
这是双指针成立的数学基础。
四、整体思路拆解
整体逻辑可以拆成三层:
- 固定第一个数
i - 在
i右侧,用双指针找left + right = -nums[i] - 在三个层面上去重
五、完整代码
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n - 2; i++) {
// 第一层去重:固定数 nums[i]
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 第二层去重:left
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
// 第三层去重:right
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return res;
}
}
六、三层去重,逐层讲清楚
这是这道题最容易写错、也最值得理解的部分。
1. 第一层去重:i 不能重复
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
含义是:
- 如果当前固定的数和上一次固定的一样
- 那后面的双指针结果一定也一样
- 直接跳过,避免重复三元组
这是在防止这种情况:
[-1, -1, 0, 1]
↑ ↑
i i+1
2. 第二层去重:left 去重
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
当我们已经找到一个合法解:
nums[i] + nums[left] + nums[right] == 0
如果 left 指向的值后面还是一样的数:
... 0, 0, 0 ...
↑ ↑
继续用它只会得到一模一样的三元组。
所以必须跳过。
3. 第三层去重:right 去重
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
逻辑和 left 完全对称。
你可以把这两层理解为一句话:
当前解已经用过了,这一整段相同的数都不再有价值
七、为什么要最后再 left++ / right--?
left++;
right--;
前面的 while 只是“跳过重复值”,
但当前这对 (left, right) 已经用过了,必须整体向中间推进,继续找新解。
八、时间与空间复杂度
-
时间复杂度:
O(n²)- 外层一个
i - 内层双指针线性扫描
- 外层一个
-
空间复杂度:
O(1)(不算结果集)
这是这道题能做到的最优解法。
九、总结一句话版
- 排序是为了双指针和去重
- 固定一个数,其余两个用左右指针夹逼
- 去重一定要分三层,缺一不可
如果你能把**“为什么要去重、去的是什么重”**讲清楚,
那这道题你就真的吃透了。