L41.【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 中是否存在三个元素 abc 使得 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--

但题目还要求了三元组不能重复,所以需要对返回的结果进行去重操作

★注意题目的细节:0 \leq nums.length \leq 3000,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次

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhangcoder

赠人玫瑰手有余香,感谢支持~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值