博客专栏地址:https://blog.youkuaiyun.com/feng964497595/category_9848847.html
github地址:https://github.com/mufeng964497595/leetcode
题目描述
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
思路解析
- 咋一看很简单,直接三层for循环嵌套就可以搞定,但是时间复杂度就是 O ( n 3 ) O(n^3) O(n3)了,肯定不能这样搞。再细看一下示例,给出的答案是已经从小到大排好序的,这不是赤裸裸的暗示我要排序么。排完序之后,a、b、c这三个数就是呈非递减顺序。我用两个for循环分别遍历a、b这两个数,记下当前在数组的下标为idx_a和idx_b,那么第三个数c肯定要大于等于b,那不就是在idx_b+1开始的有序数组中查找c么?所以把第三组for循环改成二分查找,时间复杂度就从 O ( n 3 ) O(n^3) O(n3)降到了 O ( n 2 l o g n ) O(n^2log_n) O(n2logn)了。抱着忐忑的心情跑了一下代码,居然通过了,耗时600+ms。
- 单单这样搞虽然解出了题,但是这解法对不起中等难度这个标签,于是再分析一下。第三层for循环优化了,那接下来就看下第二层怎么优化。在第一层固定的情况下,其实剩下的求解就是在有序数组中求解两数之和了。这种如果搞个map来做,那时间复杂度跟解法1其实是差不多的(hash map的话会优一些),空间复杂度还要大一点。那对于想用 O ( n ) O(n) O(n)的解法,就要考虑下从左右两端逼近的方式了。
以
[
−
6
,
−
3
,
−
1
,
0
,
1
,
3
,
5
]
[-6, -3, -1, 0, 1, 3, 5]
[−6,−3,−1,0,1,3,5]为例,求两数之和等于0的解。初始时i、j指针分别指向数组两端
此时
n
u
m
s
[
i
]
+
n
u
m
s
[
j
]
=
−
6
+
5
=
−
1
<
0
nums[i] + nums[j] = -6 + 5 = -1 < 0
nums[i]+nums[j]=−6+5=−1<0,意味着是左边的数太大了,所以要移动左指针i,得到新位置。
此时
n
u
m
s
[
i
]
+
n
u
m
s
[
j
]
=
−
3
+
5
=
2
>
0
nums[i] + nums[j] = -3 + 5 = 2 > 0
nums[i]+nums[j]=−3+5=2>0,意味着是右边的数太大了,所以要移动右指针j,得到新位置。
此时
n
u
m
s
[
i
]
+
n
u
m
s
[
j
]
=
−
3
+
3
=
0
nums[i] + nums[j] = -3 + 3 = 0
nums[i]+nums[j]=−3+3=0,即找到答案了,记录下来,然后左右指针同时移动,得到新位置。
接下来就是重复上面的步骤,直至两个指针相遇为止。
从上述算法演示可以看出,只需要遍历一次数组即可,时间复杂度 O ( n ) O(n) O(n)。最后只需要考虑下去除重复的结果集,去重也简单,只要在a、b、c这三个数看到重复值就跳过即可。
示例代码
#include <algorithm>
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
std::sort(nums.begin(), nums.end()); // 排序,控制结果集三个数从小到大排列
vector<vector<int>> res;
int len = nums.size();
for (int i = 0; i < len; ++i) {
if (i != 0 && nums[i] == nums[i - 1]) { // 控制第一个数不重复
continue;
}
int j = i + 1, k = len - 1;
while (j < k) {
int calc = nums[i] + nums[j] + nums[k];
if (j > i + 1 && nums[j] == nums[j - 1]) { // 控制第二个数不重复
++j;
} else if (k < len - 1 && nums[k] == nums[k + 1]) { // 控制第三个数不重复
--k;
} else if (calc > 0) { // 求和大于0,在第一个数不变的情况下,意味着第三个数偏大
--k;
} else if (calc < 0) { // 求和小于0,在第一个数不变的情况下,意味着第二个数偏小
++j;
} else {
vector<int> tmp = {nums[i], nums[j], nums[k]};
res.push_back(tmp);
++j;
--k;
}
}
}
return res;
}
};
PS:
ipad pencil买了这么久,终于派上用场了,画示意图确实方便很多,不需要visio调得要死要活哈哈哈