L37.【LeetCode题解】三数之和(双指针思想)
目录
1.题目
2.分析
算法
方法1:使用set去重
版本1代码
方法2:利用数组的有序性来去重
操作left和right
操作bound
其他问题
1.如果和<0,left++后需要去重吗? 同理,如果和>0,right--后需要去重吗?
2.如果和==0,是只移动left还是只移动right还是两个指针都移动?
如果只移动其中任何一个指针
如果一次性移动两个指针
版本2代码
版本2代码的小优化
3.验证指针移动的效率问题
和为0时移动动两个指针
只移动其中任何一个指针
1.题目
https://leetcode.cn/problems/1fGaJU/description/
给定一个包含
n
个整数的数组nums
,判断nums
中是否存在三个元素a
,b
,c
,使得a + b + c = 0
?请找出所有和为0
且 不重复 的三元组。示例 1:
输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]]示例 2:
输入:nums = [] 输出:[]示例 3:
输入:nums = [0] 输出:[]提示:
0 <= nums.length <= 3000
-10^5 <= nums[i] <= 10^5
注意:本题与主站 15 题相同:15. 三数之和 - 力扣(LeetCode)
2.分析
算法
从L36.【LeetCode题解】查找总价格为目标值的两个商品(剑指offer:和为s的两个数字) (双指针思想,内含详细的优化过程)文章得到的经验来看,显然先排序,利用有序数组的单调性使用三指针:将其中一个指针bound固定不动,使用左右指针left和right来移动,如果满足题目条件,就将{nums[left],nums[right],nums[bound]}尾插到类型为vector<vector<int>> 的ret中
移动的方法:
1.如果nums[left]+nums[right]+nums[bound]<0,需要增大结果,left++
2.如果nums[left]+nums[right]+nums[bound]<0,需要减小结果,right--
但题目还要求了三元组不能重复,所以需要对返回的结果进行去重操作
★注意题目的细节:,nums.length可以为0,直接返回{ },同理nums.length为1或2时,也直接返回{ },这个单独处理
但https://leetcode.cn/problems/3sum/题的nums.length最小取3,这里要注意
方法1:使用set去重
之前在CC42.【C++ Cont】STL库的红黑树set的使用文章提到过:
"set与multiset 的区别:set不能存相同元素,multiset可以存相同的元素,其余的使用方式完全一致.因此,可以用set来给数据去重"
版本1代码
class Solution {
public:vector<vector<int>> threeSum(vector<int>& nums) {set<vector<int>> tmp;vector<vector<int>> ret;if (nums.size()==0)return ret;sort(nums.begin(),nums.end());for (int bound=nums.size()-1;bound>1;bound--){int left=0;int right=bound-1;while (left<right){int sum=nums[left]+nums[right]+nums[bound];if (sum>0)right--;else if (sum<0)left++;else//sum==0{if (tmp.count({nums[left],nums[right],nums[bound]})==0){tmp.insert({nums[left],nums[right],nums[bound]});ret.push_back({nums[left],nums[right],nums[bound]});} left++; //break;}}}return ret;}
};
提醒:注意到break;语句被注释掉了,这里找到一个不能直接退出循环! 需要找到所有满足条件的三元组
提交结果:
*面试时不建议使用此法来去重,没有达到训练的要求
方法2:利用数组的有序性来去重
例如给定以下数组[-1,0,1,2,1,-4],排过序后为[-4,-1,0,1,1,2]
操作left和right
以下两种情况均满足要求:
显然需要去重,right前后都是-1,需要跳过重复的情况,可以比较right前后的值是否相同,使用循环来跳过所有重复的情况,因为重复的元素都是在一起的,有以下代码:
right--;
//right--后,right之前为right+1,比较--前和--后right指向的值是否相同
while(nums[right]==nums[right+1])right--;
但这样写会有潜在的问题:right可能会越过left导致left<=right,甚至是对数组越界访问,因此还需要一个条件left>right
right--;
while(nums[right]==nums[right+1] && left<right)right--;
同理left++后也有可能指向相同的值,代码如下:
left++;
while(nums[left]==nums[left-1] && left<right)left++;
left>right这个思想曾经在120.【C语言】数据结构之快速排序(详解Hoare排序算法)文章中提到过的单趟排序的代码,内循环由两个条件限制:
//单趟快速排序int key_i = left;while (left < right){//由于key_i==left,因此right指针先走//右边找小while (left < right && arr[right] >= arr[key_i]){right--;}//左边找大while (left < right && arr[left] <= arr[key_i]){left++;}Swap(&arr[left], &arr[right]);}Swap(&arr[key_i], &arr[left]);
操作bound
注意bound++后也有可能指向和之前相同的值,因此也要对bound去重,这里容易忽视
其次bound不能一直++,也有可能越界,bound的最大取值为 nums.size()-3,要为nums[left]和nums[right]留空位
bound++;
while(nums[bound-1]==nums[bound] && bound<nums.size()-2)bound++;
其他问题
1.如果和<0,left++后需要去重吗? 同理,如果和>0,right--后需要去重吗?
答:去重指的是:在三元组和为0的情况下且至少有两个三元组的元素相同时,需要去重,前提是和为0,如果>0或<0,不需要去重
2.如果和==0,是只移动left还是只移动right还是两个指针都移动?
从正确性上来说都没有问题,下面进行分析:
如果只移动其中任何一个指针
这里以left为例子:因为要去重,所以要移动到和之前指向的值有所不同的位置,当left移动到新的位置时,和必然不为0,可以交给下一次循环时判断并移动指针
以[-5,0,1,1,2,2,3,3,4,4]为例分析:
如果一次性移动两个指针
仍然以[-5,0,1,1,2,2,3,3,4,4]为例分析:
感觉上一次性移动两个指针效率比只移动其中任何一个指针要高一点,在文章的最后会验证此想法
版本2代码
class Solution {
public:vector<vector<int>> threeSum(vector<int>& nums) {if (nums.size()<3)return {};sort(nums.begin(),nums.end());vector<vector<int>> ret;int left,right,bound=0;while(bound<nums.size()-2){left=bound+1;right=nums.size()-1;while (left<right){if (nums[bound]+nums[left]+nums[right]>0)right--; else if (nums[bound]+nums[left]+nums[right]<0)left++;else{ret.push_back({nums[bound],nums[left],nums[right]});left++;while(nums[left]==nums[left-1]&&left<right)left++;right--;while(nums[right]==nums[right+1] && left<right)right--;}}bound++;while(nums[bound-1]==nums[bound]&&bound<nums.size()-2)bound++;}return ret;}
};
提交结果:
版本2代码的小优化
当nums[bound]>0时可以直接退出循环,一定找不到符合要求的三元组,直接返回即可,此操作可以加在循环的最后:
while(bound<nums.size()-2)
{//......while (left<right){if (nums[bound]+nums[left]+nums[right]>0)right--; else if (nums[bound]+nums[left]+nums[right]<0)left++;else{//......}}bound++;while(nums[bound-1]==nums[bound]&&bound<nums.size()-2)bound++;if (nums[bound]>3)return ret;
}
3.验证指针移动的效率问题
可以写个测试程序:统计三个指针bound、left和right的移动次数:
到leetcode上获取数据量大的测试用例,由于在leetcode上提交时网站会报告没有通过的测试用例,因此可以通过写条件判断:指定获取150个元素以上的测试用例:
由于测试代码较大,这里给出下载链接,提取码: hhrmhttps://pan.baidu.com/s/1i5juwMMvY4SXrqT-OOWKOw?pwd=hhrm
和为0时移动动两个指针
运行结果:
只移动其中任何一个指针
运行结果:
其实没有区别,和为0时指针移动的次数是一样的,都是4461737次