题目描述
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
解题思路
本题与两数之和类似,但是做法不尽相同。
题目中要求找到所有不重复且和为 0的三元组,这个不重复的要求使得我们无法简单地使用三重循环枚举所有的三元组。这是因为在最坏的情况下,数组中的元素全部为0,即
[0, 0, 0, 0, 0, …, 0, 0, 0]
此时可以看到任意一个三元组之和都为0,时间复杂度为O(N^3)。
为达到不重复的要求,我们需要保证:
- 第二重循环枚举的元素不小于第一重循环枚举的元素。
- 第三重循环枚举的元素不小于第二重循环枚举的元素。
由此,我们很容易得到基本的解题思路。
基本思路:排序 + 三重循环
我们可以将数组中的元素从小到大进行排序,随后使用普通的三重循环就可以满足上面的要求。
同时,对于每一重循环而言,相邻两次枚举的元素不能相同,否则也会造成重复。举个例子,如果排完序的数组为
[0, 1, 2, 2, 2, 3]
我们使用三重循环枚举到的第一个三元组为 (0,1,2),如果第三重循环继续枚举下一个元素,那么仍然是三元组 (0,1,2),产生了重复。因此我们需要将第三重循环「跳到」下一个不相同的元素,即数组中的最后一个元素 3,枚举三元组 (0,1,3)。对于第一重和第二重循环也做同样的去重处理(实际就是直接跳过)。
伪代码如下:
nums.sort()
for first = 0 .. n-1
// 只有和上一次枚举的元素不相同,我们才会进行枚举
if first == 0 or nums[first] != nums[first-1] then
for second = first+1 .. n-1
if second == first+1 or nums[second] != nums[second-1] then
for third = second+1 .. n-1
if third == second+1 or nums[third] != nums[third-1] then
// 判断是否有 a+b+c==0
check(first, second, third)
改进思路:排序 + 二重循环(双指针)
通过上面的分析我们可以看出,时间复杂度依然是O(N^3)。通过分析,如果我们固定了前两重循环枚举到的元素 a 和 b,那么只有唯一的 c 满足 a+b+c=0。当第二重循环往后枚举一个元素 b′时,由于 b′>b,那么满足 a+b′+c′=0的 c′一定有 c′<c,即 c′在数组中一定出现在 c的左侧。也就是说,我们可以从小到大枚举 b,同时从大到小枚举 c,即第二重循环和第三重循环实际上是并列的关系。
伪代码如下:
nums.sort()
for first = 0 .. n-1
if first == 0 or nums[first] != nums[first-1] then
// 第三重循环对应的指针
third = n-1
for second = first+1 .. n-1
if second == first+1 or nums[second] != nums[second-1] then
// 向左移动指针,直到 a+b+c 不大于 0
while nums[first]+nums[second]+nums[third] > 0
third = third-1
// 判断是否有 a+b+c==0
check(first, second, third)
左指针b每次会向右移动一个位置,而右指针c会向左移动若干个位置。此时时间复杂度降低为O(N^2)。
还应当注意一点:左指针始终应当小于右指针。
继续改进
观察上面的伪代码我们发现,在第二层循环遍历左指针时,右指针可能会移动若干个位置,导致了循环的嵌套。虽然没有导致时间复杂度的增加,但是代码不美观。
我们重新梳理一遍思路,发现第二重循环可以如此判断:
- 固定a,当a+b+c < 0时,说明左指针值太小,右移左指针。
- 当a+b+c > 0时,说明右指针值太大,左移右指针。
- 当a+b+c = 0时,得到可行解,同时移动左右指针,并进行去重处理(跳过)。
这样,第二重循环可根据a+b+c 和 0 之间的关系进行判断。(如果依然不是很理解,可以看下本篇算法的难点这一部分。)
示例的遍历过程如下图所示:
说明:在最后一行跳过了第一重遍历中重复的 -1。
算法的难点
在看这道题的官解时我有这样的一个疑问(如果没有这个疑问可以跳过这一部分):
有这样的数组:
[a, b, b’, b",……, c", c’, c]
设a+b+c < 0, a+b’+c > 0
若a+b’+c’ < 0, 按照双指针应当判断a+b"+c’ 和 0 之间的关系。(即右指针不动,左指针右移)
那么是否需要判断a+b’+c 和 0 的关系呢?(即左指针不动,右指针右移)
同理,若a+b’+c’ > 0, 按照双指针应当判断a+b’+c" 和 0 之间的关系。(即左指针不动,右指针左移)
那么是否需要判断a+b+c" 和 0 的关系呢?(即右指针不动,左指针左移)
证明如下:
综上分析:利用双指针,只需要从小到大枚举左指针,同时从大到小枚举右指针,不开历史的倒车。
代码
不美观版本
根据改进思路:左指针b每次会向右移动一个位置,而右指针c会向左移动若干个位置写出如下代码。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums)
{
vector<vector<int>> ans;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size(); i++)
{
if(i >= 1 && nums[i] == nums[i - 1])
continue;
if (nums[i] > 0)
break;
int left = i + 1;
int right = nums.size() - 1;
while(left < right)
{
if(nums[i] + nums[left] + nums[right] < 0)
left++;
while(left < right && nums[i] + nums[left] + nums[right] > 0)
right--;
if(left < right && nums[i] + nums[left] + nums[right] == 0)
{
ans.push_back({nums[i], nums[left], nums[right]});
left++; right--;
while(left < right && nums[left] == nums[left - 1])
left++;
while(left < right && nums[right] == nums[right + 1])
right--;
}
}
}
return ans;
}
};
美观版本
根据继续改进思路:第二重循环根据 a+b+c 和 0 之间的关系进行判断。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums)
{
vector<vector<int>> ans;
sort(nums.begin(), nums.end());
//第一重循环
for (int i = 0; i < nums.size(); i++)
{
if (i > 0 && nums[i] == nums[i - 1]) continue;
if (nums[i] > 0) break;
int left = i + 1, right = nums.size() - 1;
//第二重循环
while (left < right)
{
//小于0,右移左指针
if (nums[left] + nums[right] + nums[i] < 0) ++left;
//大于0,左移右指针
else if (nums[left] + nums[right] + nums[i] > 0) --right;
//等于0,记录可行解,去重
else
{
ans.push_back({nums[i], nums[left], nums[right]});
++left, --right;
while (left < right && nums[left] == nums[left - 1]) ++left;
while (left < right && nums[right] == nums[right + 1]) --right;
}
}
}
return ans;
}
};