https://leetcode-cn.com/problems/3sum/
分析
这道题与Two Sum很像,我们很容易想到利用Two Sum的程序来解答这个问题,对于a+b+c = 0,有-c = a+b,这相当于target设置为-c的Two Sum问题。区别在于,a和b的值不是唯一的,即这个Two Sum的答案不唯一。
除了找到所有正确的a和b外,还要解决三元组重复的问题。
解法一
我们依然考虑在Two Sum的基础上解决这个问题。
首先,Two Sum的核心代码是:
for (int i = 0; i < nums.size(); ++i) {
if (m.find(target - nums[i]) != m.end())
return {m[target - nums[i]], i};
m[nums[i]] = i;
}
我们把return
那一行做一些修改,改为添加一个三元组到答案中,就可以作为本题的核心代码了。
问题在于,这样做会导致大量重复的三元组,有两种情况的重复:
- 调换元素顺序导致的重复:如[-1,0,1]和[0,-1,1]本质上是同一个三元组
- 不同元素的值相同导致的重复:如nums=[-4,2,2,2],会导致两个[-4,2,2]的产生;nums=[-1,-1,0,1],会导致两个[-1,0,1]的产生
情况1比较容易解决,我们在添加三元组时,只添加a <= b <= c这样的[a,b,c]到答案中,即指定答案中三元组的顺序。
但这样做是不够的,不仅解决不了情况2,当多个元素值相同时,连情况1也解决不了。如nums=[0,0,0],这时满足a == b == c,我们加的限制条件也就没有用了。
采用排序的方法能够很好地解决这两种情况的重复。
对于排序之后的数组,我们使用双层循环,外层循环用于寻找target值,内层循环的逻辑就是Two Sum的逻辑,借助哈希表来寻找sum为target的两个元素。
我们保证内层循环枚举到的元素不小于外层循环的元素,就可以解决情况1。对于每重循环还要保证相邻两次遍历到的元素值不相同,这是为了解决情况2。伪代码如下:
nums.sort()
for i = 0...n-1
if i > 0 && nums[i] == nums[i-1] continue;
for j = i+1...n-1
if j > i+1 && nums[j] == nums[j-1] continue;
// a=nums[j], 用哈希表找对应的b
需要注意的是,如果我们像Two Sum中那样一边遍历一边向哈希表中插入元素,可以满足a <= b <= c,即解决情况1。因为在遍历到b时target-b还不在哈希表中,而遍历到c时target-c在哈希表中。
这样做貌似效率很高,但会错过[-4,2,2]这样的三元组。因为当j == 1时nums[j]不在哈希表中,而j=2时又因为nums[j] == nums[j-1],不会参与判断。我们为了去除重复结果矫枉过正了。解决办法是在排序之后先将数组元素插入哈希表中。
我们增加了一个判断条件m[sum - nums[j]] > j
来保证a <= b <= c。对于[-4,2,2]这种元素有相同值的情况,也是满足的,因为经过排序后最终覆盖哈希表的肯定是index最大的那个元素。最终,我们要添加的三元组是[nums[i], nums[j], nums[m[target - nums[j]]]]
。
代码
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
unordered_map<int, int> m;
for (int i = 0; i < nums.size(); ++i) m[nums[i]] = i;
for (int i = 0; i < nums.size(); ++i) {
if (i > 0 && nums[i] == nums[i - 1]) continue;
int sum = -nums[i];
for (int j = i + 1; j < nums.size(); ++j) {
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
if (m.find(sum - nums[j]) != m.end() && m[sum - nums[j]] > j) {
result.push_back({nums[i], nums[j],nums[m[sum - nums[j]]]});
}
}
}
return result;
}
};
复杂度分析
时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n ) O(n) O(n)。注意用的排序算法时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)。
解法二
去除重复三元组的思路与解法一相同,但我们不使用哈希表,而是使用双指针来优化暴力解法。
暴力解法是3重循环,同时满足两个条件:
- 内层遍历循环元素不小于外层循环遍历的元素
- 对于每一重循环,相邻遍历的元素值不相同
即解法一中的核心想法。
3重循环获取到a,b,c,如果a+b+c = 0,它们就是一个满足条件的三元组。自然,a,b对应唯一的c,当b增大时,c减小,因此,我们可以在从左往右枚举b的同时从右往左枚举c。
- 当a+b+c > 0时,把右指针向左移动,取更小的c
- 当a+b+c = 0时,把左指针向右移动,取更大的b,此时肯定有a+b+c > 0
- 当a+b+c < 0时,可以确定这个a,b没有对应的c,把左指针向右移动,取更大的b。是否要把右指针向右移动呢?不用。首先要明白为什么会有a+b+c < 0出现,肯定因为1.导致的,如果我们又把右指针往右移,那么又会得到a+b+c > 0的结果,最终还得把右指针移到现在这位置来
这样一来,就可以把第二重和第三重循环压缩成一个循环了。当然,始终要保证b <= c,否则退出循环。
代码
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); ++i) {
if (i > 0 && nums[i] == nums[i - 1]) continue;
int right = nums.size() - 1;
for (int left = i + 1; left < right; ++left) {
if (left > i + 1 && nums[left] == nums[left - 1]) continue;
while (nums[i] + nums[left] + nums[right] > 0 && left < right) right--;
if (nums[i] + nums[left] + nums[right] == 0 && left < right) {
result.push_back({nums[i], nums[left], nums[right]});
}
}
}
return result;
}
};
复杂度分析
时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( l o g n ) O(logn) O(logn),排序的空间复杂度为 O ( l o g n ) O(logn) O(logn)。