15 三数之和 Medium
找出一个数组中,所有满足和为0,且不重复的三元组
这道题的难度在于不重复,一个思想是两层遍历,先定位第一个数,然后确定target,再使用两数之和的思想求解之后的数字
这道题的难度在于去重和剪枝,使用哈希数组细节很多
细节之一:如果数组中有重复的数字,应该怎么办?,比如{-1,-1,1,0},理论上答案是{-1,1,0},但是按照我们此前的思想,就会有两个答案,所以要先去
随想录中给出的哈希算法如下:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
unordered_set<int> set;
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 三元组元素b去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
三元组中去除元素a的重复性比较好理解;
去除元素b的时候,为什么要求三个index上的数呢?思考这个问题要从“不像考虑a时,前后去重就可以了”的角度思考。思考一下,如果只比较 j 和 j - 1的数字,如果和nums[i]求和时,nums[i] + nums[j] + nums[j - 1]恰好就等于0呢?
所以,必须得考虑从三个相同的元素中,再去除重复的元素。
那为什么在set中找不到c = -(a + b)
时,要记录当前的b值呢?
我们这么考虑,假如有这么三个数字a, b1, b2
,且满足a + b1 != 0, a + b1 + b2 = 0
当遍历到nums[j] = b1,时,c = -(a + b1)
,此时set中还没有数字,所以要把b1
记录在set中
此后当遍历到nums[j] = b2
时,c = -(a + b2)
,且又因为b1 = -(a + b2)
,所以此时就找到了三元组
在LeetCode题解中提交时,时间和空间的复杂度都是很高的
接下来就可以看看几乎所有人,都是那么写的,双指针法了
首先还是要先排序,然后固定a = nums[i],关键在于如何找b和c
双指针法的思想是,从除了nums[i]的数之外,设置left = i + 1,和right = nums.size() - 1,此时b = nums[left],c = nums[right]
如果a + b + c > 0,那么就说明三数之和大了点,就要令right左移,令三数之和可能减小(可能的原因是因为有重复的数字)
反之,如果a + b + c < 0,那么就说明三数之和小了点,就要令left右移,令三数之和可能增大(可能的原因是因为有重复的数字)
直到left和right相遇为止。
需要注意的是,这道题还是需要去重a,b,c,只是相较于使用哈希算法,好想了那么一些。
时间复杂度O(n^2),代码如下:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
int left, right;
vector<vector<int>> result = {};
for(int i = 0; i < nums.size(); i++) {
if(nums[i] > 0) break;
// 去重a
if(i > 0 && nums[i] == nums[i-1]) continue;
left = i + 1;
right = nums.size() - 1;
while(left < right) {
if(nums[i] + nums[left] + nums[right] < 0)
left++;
else if (nums[i] + nums[left] + nums[right] > 0)
right--;
else if(nums[i] + nums[left] + nums[right] == 0){
result.push_back({nums[i], nums[left], nums[right]});
left++;
right--;
//去重b
while(left<right && nums[left] == nums[left-1]) left++;
//去重c
while(left<right && nums[right] == nums[right+1]) right--;
}
}
}
return result;
}
18 四数之和
和三数之和不同的是,我们已经不需要考虑哈希了
这道题还有一点不同的是,目标值不是零
按照三数之和的思想,再套一层循环就可以了
以下是随想录中的原话:
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n2),四数之和的时间复杂度是O(n3) 。
那么一样的道理,五数之和、六数之和等等都采用这种解法。
如果要归在算法的类型中的话,目前已经遇到很多使用双指针思想的题目了
以下为随想录中给出的解法
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int k = 0; k < nums.size(); k++) {
if (nums[k] > target && nums[k] >= 0) {
break;
}
// 对nums[k]去重
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
for (int i = k + 1; i < nums.size(); i++) {
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
break;
}
// 对nums[i]去重
if (i > k + 1 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
if ((long) nums[k] + nums[i] + nums[left] + nums[right] > target) {
right--;
} else if ((long) nums[k] + nums[i] + nums[left] + nums[right] < target) {
left++;
} else {
result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
// 对nums[left]和nums[right]去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
}
return result;
}