本题依旧是使用双指针解决,需要用到上一个题目两数之和的知识,没看过上一篇文章的不用着急,可以先接着往下看,看不懂了再去看两数之和内个题目,这是链接和为s的两个数。本题的难度要比两数之和难上许多,当然后面还有个四数之和,他们的关系是层层递进的。
讲完四数之和后,我们的双指针系列也到了尾声,下一个系列是滑动窗口。
即数组中的每一个元素只能使用一次,不能重复使用,而且还得满足这三个数加起来等于0.
虽然 (-1) + 0 + 1 = 0 和0 + 1 + (-1) = 0顺序不一样,但它们的三个元素值是一样的,所以是重复的,只保留一个就可以。
可能会有小伙伴问,为什么每次都要讲暴力解法呢?直接来个最优的解法不就行了。我想说的是,我们的最优解往往是在暴力解法的基础上进行优化得来的,所以在讲最优解之前,先熟悉一下暴力解法我认为还是有必要的。
暴力嘛。那肯定是直接三层for循环把每一种情况都遍历一遍,看看符不符合结果,就是时间复杂度会有点高,是O(n^3)。
另一个问题是去重,我们可以讲结果先排个序,检测两组答案中的元素一样不一样金星去重(可以利用set容器去重)。
例如:上面例子中的[-1,0,1]、[0,1,-1]、将每一组中的元素进行排序题目就变成了[-1,0,1]、[-1,0,1],这样利用set就可以很容易的进行去重了。
具体代码就不编写了,我们重点看优化后的解法二。
1.我们可以先对数组排个序,因为排序之后,我们列出来的元素就是有序的,而且相同的数字会挨在一起,便于我们去重。
2. 要求三数之和为0,我们可以先固定一个数a,然后在后面的区间里找两数之和等于-a
。两个数之和等于-a那不就是我上篇文章刚讲过的内容嘛,这样问题不就迎刃而解了。不熟悉的小伙伴可以先去看和为s的两个数这篇文章,比较简单,大家可以迅速看完再回来。
总体流程:
两数之和对应代码:
while(left<right)//两数之和的方法
{
int n=nums[right]+nums[left];
if(target>n) left++;
else if(target<n) right--;
else //else里面的内容可以暂且不管,后面会详细讲解。
{
ret.push_back({nums[i],nums[left],nums[right]});
left++;
right--;
//去重操作,left和right
while(left<right && nums[left]==nums[left-1]) left++;
while(left<right && nums[right]==nums[right+1]) right--;
}
1.排序 2.固定一个数a 3.在该数后面的区间内利用双指针算法快速找到两个数之和等于-a即可。
其中有个小优化,那就是固定的内个数a一定是<=0的,因为数组是有序的,如果a>0那么a后面区间的元素肯定都>0,加起来是不可能等于0的,因此我们只需要讨论a<=0的情况
处理细节问题
1.去重:
(1)找到一种结果后left和right指针要跳过重复元素。
以下图为例分析:
我们可以看到left指向的元素和left-1指向的元素相同,那三个数已经有两个数固定了,最终right找下的第三个数肯定也是一样的啊,所以一定会重复,此时我们只需要继续让left++,直到它和left-1指向的元素不再相等就可以了。我们可以这样写:
while (nums[left] == nums[left - 1])
left++;
同理right也是一样,如果right指向的元素和right+1指向的元素相同,那三个数已经有两个数固定了,最终left找下的第三个数肯定也是一样的啊,所以一定会重复,此时我们只需要继续让right–,直到它和right+1指向的元素不再相等就可以了。我们可以这样写:
while (nums[right] == nums[right + 1])
right--;
但是这里有个问题那就是一直left++或者right–越界了怎么办,所以我们要加个限制条件left<right防止越界。因此完整的代码应该这样写:
// 去重:left right
while (left < right && nums[left] == nums[left - 1])
left++;
while (left < right && nums[right] == nums[right + 1])
right--;
(2)当使用完一次双指针算法后,i也要跳过重复元素。
以下图为例分析:
当第一个-4处理完之后要i++我们发现第二个也是-4,那不就i和i-1指向的元素就又一样了,这就又会导致重复为了解决问题,我们依旧让i继续++,直到i和i-1指向的元素不同为止。
我们可以在for循环内部这样写:
// 去重:i
i++;
while (i < n && nums[i] == nums[i - 1]) i++;
既然for循环内部实现i++了那我们就不这样写了:
for (int i = 0; i < n;i++)
而是:
for (int i = 0; i < n;)
2.不漏:
找到一种结果后不要停,缩小区间继续寻找。
即left++;right–
部分解释:
ret.push_back({nums[i],nums[left],nums[right]});
//因为返回值是vector<vector<int>>,所以在ret调用push_back时括号内也应该放一个vector<int>类的对象。
//括号内的{nums[i],nums[left],nums[right]}会自动构建一个vector<int>类的对象,然后被push_back。
C++完整代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums)
{
vector<vector<int>> ret;
//1.排序
sort(nums.begin(),nums.end());
//2.利用双指针解决问题的
int n=nums.size();
for(int i=0;i<n;)//固定数a
{
int left=i+1,right=n-1,target=-nums[i];
if(nums[i]>0) break;//小优化
while(left<right)//两数之和的方法
{
int n=nums[right]+nums[left];
if(target>n) left++;
else if(target<n) right--;
else
{
ret.push_back({nums[i],nums[left],nums[right]});
left++;
right--;
//去重操作,left和right
while(left<right && nums[left]==nums[left-1]) left++;
while(left<right && nums[right]==nums[right+1]) right--;
}
}
i++;
//去重i
while(i<n && nums[i]==nums[i-1]) i++;
}
return ret;
}
};
运行结果:
Java完整代码:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ret = new ArrayList<>();
// 1. 排序
Arrays.sort(nums);
// 2. 利用双指针解决问题
int n = nums.length;
for (int i = 0; i < n;) // 固定数 a
{
if (nums[i] > 0)
break; // 小优化
int left = i + 1, right = n - 1, target = -nums[i];
while (left < right) {
int sum = nums[left] + nums[right];
if (sum > target)
right--;
else if (sum < target)
left++;
else {
// nums[i] nums[left] num[right]
ret.add(new ArrayList<Integer>(Arrays.asList(nums[i],
nums[left], nums[right])));
left++;
right--; // 缩小区间继续寻找
// 去重:left right
while (left < right && nums[left] == nums[left - 1])
left++;
while (left < right && nums[right] == nums[right + 1])
right--;
}
}
// 去重:i
i++;
while (i < n && nums[i] == nums[i - 1])
i++;
}
return ret;
}
}
运行结果:
勤加练习!
本文到这里就结束了,希望友友们可以支持一下一键三连哈。嗯,就到这里吧,再见啦!!!