[双指针|模拟] leetcode 15 三数之和
1.题目
题目链接
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
2.分析
2.1.直观思路
直接利用三层for循环枚举a、b、c,然后观察a + b + c是否为0:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
int n = nums.size();
for(int i = 0; i < n; i++) {
for(int j = i + 1; j < n; j++) {
for(int k = j + 1; k < n; k++) {
if(nums[i] + nums[j] + nums[k] == 0) {
res.push_back({nums[i], nums[j], nums[k]});
}
}
}
}
return res;
}
这种做法在允许重复的情况下是没有问题的,然而题目要求不能有重复。例如对样例而言,上述代码的输出会多出一个[0,1,-1],与[-1,0,1]重复。
那么如何解决重复问题呢?
2.2.问题转化
三数之和问题可以转化为两数之和问题。即:
a + b + c = 0 =>
b + c = -a
也就是找到两数b、c的和为-a。因此,遍历a可能的n种情况即可将问题转化为相对简单两数之和问题:
//遍历a
for (int i = 0; i < n; i++) {
//target = -a
int target = -nums[i];
//遍历b、c...
for (int j = i + 1; j < n; j++) {
}
}
2.3.重复问题
观察我们之前重复问题出现的原因。对于输入:
[-1, 0, 1, 2, -1, -4]
-1会被遍历到两次,因此带来了重复问题。
一种解决方案是利用数据结构set,这里我们采取另一种方法——排序。可以得到:
[-4, -1, -1, 0, 1, 2]
这样,重复的数必定相邻,我们只需要判断当前遍历到的数与上一个数是否相等即可。例如判断a是否重复:
//nums[i]与上一个数nums[i - 1]相等则直接跳过
//i > 0是为了防止i - 1 < 0 导致数组越界
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
2.4.枚举策略
对于b、c的遍历,可以简单采用原来三层for循环的思路:
//遍历b
for (int j = i + 1; j < n; j++) {
//遍历c
for (int k = j + 1; k < n; k++) {
}
}
这样做的时间复杂度是O(n2),有没有更好的做法?
一种方式是将b、c改为一左一右的双指针,即:
int k = n - 1;
//遍历b
for (int j = i + 1; j < n; j++) {
//遍历c
while(???){
k--;
}
}
这样做可以将遍历b、c的复杂度降至O(n)。
合理性证明:
令b = nums[j], c = nums[k]。如果当前b + c != -a,要想使b + c = a,有两种情况:
- b + c < -a,此时若j右移,b增大,b + c有可能等于-a;若k左移,c减小,b + c不可能等于-a。
- b + c > -a,此时若j右移,b增大,b + c不可能等于-a;若k左移,c减小,b + c有可能等于-a。
因此,在b + c < -a时使j右移,反之使k左移即可。这一双指针的思想与盛水最多的容器十分相似。
3.代码
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int n = nums.size();
sort(nums.begin(), nums.end());
vector<vector<int>> res;
//遍历a
for (int i = 0; i < n; i++) {
//如果nums[i]与nums[i-1]相同,则跳过,以防重复
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
//target = -a
int target = -nums[i];
int k = n - 1;
//遍历b
for (int j = i + 1; j < n; j++) {
//如果nums[j]与nums[j-1]相同,则跳过,以防重复
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
//如果b + c > -a,需要将c减小(即指针k左移))
while (j < k && nums[j] + nums[k] > target) {
k--;
}
//左右指针相遇,退出
if (j == k) {
break;
}
if (nums[j] + nums[k] == target) {
res.push_back({nums[i], nums[j], nums[k]});
}
}
}
return res;
}
};