双指针算法第二弹(查找总价格为目标值的两个商品-和为s的两个数字 三数之和 四数之和)

系列文章目录

《双指针算法第一弹(移动零 复写零 快乐数)》链接:http://t.csdnimg.cn/Nqdvn


目录

系列文章目录

前言

1. 查找总价格为目标值的两个商品

(1)题目及示例

(2)思路(由浅入深)

2. 三数之和

(1)题目及示例

(2)一般思路

(2)双指针优化

3. 四数之和

(1)题目及示例

(2)一般思路

(3)双指针优化

总结


前言

本篇文章开启双指针算法第二弹,一起来感受双指针算法的魅力。这三道OJ题思路相似,难度由易到难,可以按照下面顺序自己挑战一下。每道题目中都带有链接,不用再去Leetcode寻找了!


1. 查找总价格为目标值的两个商品

(1)题目及示例

题目:购物车内的商品价格按照升序记录于数组 price。请在购物车中找到两个商品的价格总和刚好是 target。若存在多种情况,返回任一结果即可。

链接:. - 力扣(LeetCode)

示例 1:

输入:price = [3, 9, 12, 15], target = 18

输出:[3,15] 或者 [15,3]

示例 2:

输入:price = [8, 21, 27, 34, 52, 66], target = 61

输出:[27,34] 或者 [34,27]

(2)思路(由浅入深)

分析:这道题目让我们在给出的数组中寻找两个数字的总和刚好是target。其中这个数组是个升序数组,已经排好序了。

如果我们用正常思维,最先想到应该是暴力解法。写两层for循环,第一层循环固定第一个数,第二层循环从固定数后一个数开始寻找符合题目要求的数,然后固定的数从首元素开始到倒数第二个元素结束。两层for循环,最坏的情况是遍历n-1,n-2,……,1,加起来就是N^{2}级别,时间复杂度O(N^{2}),空间复杂度是O(1)。如果你把下面的代码写到Leetcode中去提交,大概率过不了,会超出时间限制。

    vector<int> twoSum(vector<int>& price, int target) 
    {
        int n = price.size();
        for(int i = 0; i < n - 1; i++)
            for (int j = i + 1; j < n; j++)
                if (price[i] + price[j] == target)
                    return {price[i], price[j]};

        return {-1, -1};//照顾编译器
    }

我们该如何优化呢?需要注意到题目给出的数组是升序的,我们可以利用这个性质。我们使用两个变量表示数组元素下标,用来指向数组元素。此时两元素之和为sum。

  • 如果其中一个变量加1,即指向后一个元素,因为数组是单调递增的,sum的值都会增大。
  • 如果其中一个变量减1,指向前一个元素,那么指向元素比之前的元素小,sum的值会减小。

如下图,target等于31,left和right表示数组元素的下标。我们不让left和right一开始都指向首元素,让left指向首元素,right指向末尾元素。sum此时为首尾元素之和,跟target无非就大于,小于和等于三种关系。

  • 下图中,sum小于target。如果使用暴力解法,right需要指向第二个元素9开始,然后往后挪动,寻找匹配的元素。可末尾元素是最大的,它和首元素相加都小于target,那么中间元素加上首元素肯定也小于target。因此,此时只需要将left++,指向后一个元素,sum才会增大,继续与target比较。

  • 同理,此时target为37,sum大于target。如果使用暴力解法,那么left固定在末尾元素之前,都会跟末尾元素相加。首元素与末尾元素相加大于target,况且中间元素全部都比首元素大,那么中间元素加上末尾元素肯定也大于target。此时,只需要right--,指向前一个元素,使得sum减小,才有可能与target相等。

  • 根据上面的分析再来实现代码,是很简单的。先定义三个变量left,right,sum,分别表示数组元素的下标和下标元素之和。使用while循环,循环条件是left小于right。
  • 循环内部就是sum赋值为两元素之和,然后跟target比较。当sum大于target时,需要减小肃穆,right--,同理sum小于target时,就是left++。
  • 如果相等,直接使用花括号返回两个元素,这样子的形式是隐士类型转换。
  • 最后再循环外面再随便返回两个值,题目中没有说明找不到的情况返回什么,但是不返回的话,编译器会报错,认为如果循环走完没有返回值,就会出问题,所以可以随便返回两个值。
    vector<int> twoSum(vector<int>& price, int target) 
    {
        int left = 0, right = price.size() - 1, sum = 0;
        while(left < right)
        {
            sum = price[left] + price[right];
            //三种情况
            if (sum > target)
                right--;
            else if (sum < target)
                left++;
            else
                return {price[left], price[right]};
                //return vector{price[left], price[right]};
        }

        return {-1, -1};//照顾编译器
    }

2. 三数之和

(1)题目及示例

题目:给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。注意:答案中不可以包含重复的三元组。

链接:. - 力扣(LeetCode)

示例 1:

输入:nums = [-1,0,1,2,-1,-4]

输出:[[-1,-1,2],[-1,0,1]]

解释:

nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。

nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。

nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。

不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]

输出:[]

解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]

输出:[ [0,0,0] ]

解释:唯一可能的三元组和为 0 。

(2)一般思路

这道题我们使用暴力枚举,写三层for循环,找出所有的三元组合。再求和,寻找和为0的三元组。但是题目还有要求,输出的三元组不能重复,所以还需要检查和为0的三元组,进行去重操作

  • 如下图,示例1中的数组,有三个和为0的数组,肉眼观察就知道前两个是重复,那程序怎么辨别呢?可以对每个符合要求的三元组进行排序,再进行比较就可以去重。
  • 但是符合要求的三元组非常多,每一个进行排序,消耗很大,效率低。因此,可以一开始就排排升序,让和为0的三元组按顺序存放,然后再使用set自动去重,也可以手动去重。

下面的代码就是按照上面的思路实现的。

  • 数组先进行了排序,排序时间复杂度O(n\log n),但是使用了三层for循环,暴力枚举出所有三元组,时间复杂度是O(n^{3})。总的时间复杂度是 O(n\log n + n^{3})。在这个时间复杂度中,n^3 项是主导项,因此可以简化为 O(n^{3})。
  • vector 存储最终的三元组,最坏情况下需要 O(n^{3}) 的空间,即所有可能的组合都不重复。set 用于去重,最坏情况下也需要 O(n^{3}) 的空间,以存储所有不重复的三元组。因此,总的空间复杂度是 O(n^{3})。
vector<vector<int>> threeSum(vector<int>& nums)
{
    vector<vector<int>> ret;
    set<vector<int>> unique; // 使用set去重
    sort(nums.begin(), nums.end()); // 先对数组进行排序

    for (int i = 0; i < nums.size(); ++i) 
        for (int j = i + 1; j < nums.size(); ++j) 
            for (int k = j + 1; k < nums.size(); ++k) 
                if (nums[i] + nums[j] + nums[k] == 0) 
                {
                    vector<int> tmp = {nums[i], nums[j], nums[k]};
                    unique.insert(tmp); // 将三元组插入set中,自动去重
                }

    // 将set中的三元组拷贝到结果数组中
    for (const auto& e : unique) 
        ret.push_back(e);

    return result;
}

(2)双指针优化

这道题其实跟上一道题解法类似,可以说是它的拓展。我们先对数组排升序,然后利用升序的性质使用双指针。不过这次需要寻找三个数字,可以固定首元素,寻找和为首元素的相反数的两个数。但是上一个题目只有寻找一对数。

在这道题中,如果找到一对符合要求的数字,还要继续寻找,直到两个变量相等,即指向同一个元素。需要注意,还要执行三个去重操作。target为固定元素的相反值,sum为left和right下标元素之和。

  • 如下图,先固定首元素,使用双指针寻找和为target的两个元素。第四个元素之前,sum小于target,所以left需要不断加1,往后移动。直到指向第四个元素时,sum等于target,left指向前一个元素,right指向后一个元素,并且需要跳过相同的数

  • 下面数组中,left和right指向的元素之和刚好为target。left++,right--,指向中间的元素,不过有两个2,相同的元素,right需要跳过去。
  • 此时left指向的元素值是1,right指向元素的值也是1,符合要求,会记录下来。left和right指向的元素相邻,说明这一轮已经结束。first需要指向后面的元素,并且它也要跳过相同的元素,避免重复

 

当我们知道需要三个去重操作之后,再去实现代码就比较容易。

  • 其中排序消耗的时间复杂度是O(n\log n),两层for循环的时间复杂度是O(n^2{}),综合起来,时间复杂度是O(n^2{})。
  • 空间上,使用了几个变量,还使用ret数组,数组的大小取决于输入数组中三元组的数量,最多也是O(n^2{})。
  • 首先对数组进行排序。其次使用for循环,固定首元素,直到倒数第三个元素。i表示固定元素的下标,我们一开始就判断,固定的元素跟前一个元素是否重复,重复就跳过,并且要注意先判断i大于0,不然会越界访问到前面内存空间并报错。
  • for循环内部就是寻找和为target的二元组,跟第一道题目类似。只不过再找到一个二元组时,也需要跳过重复的数字,进行去重操作。这里也需要注意while循环中继续的条件,先写left<right,这个是保证两个变量相等时,不会发生对数组的元素发生二次遍历。
vector<vector<int>> threeSum(vector<int>& nums) 
{
    sort(nums.begin(), nums.end());//对数组进行排序

    int n = nums.size();
    vector<vector<int>> ret;
    for (int i = 0; i < n - 2; ++i) 
    {
        if (i > 0 && nums[i] == nums[i - 1]) continue; // 跳过重复的i

        int target = -nums[i];
        int left = i + 1, right = n - 1;
        while (left < right) 
        {
            int sum = nums[left] + nums[right];
            if (sum < target) 
                ++left;
            else if (sum > target) 
                --right;
            else 
            {                  
                ret.push_back({nums[i], nums[left], nums[right]});

                ++left;
                while (left < right && nums[left] == nums[left - 1]) 
                    ++left; // 跳过重复的left

                --right;
                while (left < right && nums[right] == nums[right + 1]) 
                    --right; // 跳过重复的right
            }
        }
    }
    return ret;
}

3. 四数之和

(1)题目及示例

题目:给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

链接:. - 力扣(LeetCode)

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0

输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

输入:nums = [2,2,2,2,2], target = 8

输出:[[2,2,2,2]]

(2)一般思路

这道题目是三数之和的升级,寻找符合要求的四个数。

暴力解法也类似,先进行排序,再嵌套四层for循环,枚举出所有四元组,再求和比较是否与target相等。使用set去重,或者手动去重。这里就不在写代码了。

暴力解法中,使用sort排序时间复杂度O(n\log n),再使用了四层for循环,枚举出所有四元组,时间复杂度是O(n^{4})。总的来说,时间复杂度就是O(n^{4})。

(3)双指针优化

四数之和,是三数之和的延伸。两道题思路大体是一样的,先写两层for循环来固定两个数,下标分别为i和j,然后再转换成寻找和为target-nums[i]-nums[j]的二元组,又变成了如何解决第一道题目。不过这道题需要去重的地方有四处。

  • first固定第一个数,跳过重复元素。
  • second固定第二个数,跳过重复元素。
  • left和right双指针,跳过重复元素。

实现代码时,需要注意Leetcode给出的测试用例中,target超出int整型范围,会造成溢出,需要换成long long来定义变量。

    vector<vector<int>> fourSum(vector<int>& nums, int target) 
    {
        sort(nums.begin(), nums.end());//对数组进行排序

        int n = nums.size();
        vector<vector<int>> ret;//存储四元组
        for (int i = 0; i < n - 3; ++i) //固定第一个数
        {
            if (i > 0 && nums[i - 1] == nums[i])//跳过重复的数字
                continue;

            //利用三数之和
            for (int j = i + 1; j < n - 2; ++j) //固定第二个数
            {
                if (j > i + 1 && nums[j - 1] == nums[j])//跳过重复的数字,需要注意不能写成j>0
                    continue;

                int left = j + 1, right = n - 1;
                //示例target太大,会整型溢出,用long long类型转换一下
                long long aim = (long long)target - nums[i] - nums[j];
                // 利用双指针解决
                while(left < right)
                {
                    int sum = nums[left] + nums[right];
                    if (sum > aim)
                        --right;
                    else if (sum < aim)
                        ++left;
                    else
                    {
                        ret.push_back({nums[i], nums[j], nums[left], nums[right]});

                        ++left;
                        while(left < right && nums[left] == nums[left - 1])
                            ++left;// 跳过重复的left

                        --right;
                        while(left < right && nums[right] == nums[right + 1])
                            --right;// 跳过重复的right
                    }
                }
            }
        }
        return ret;
    }


总结

通过这三道题目的锻炼,想必对双指针算法有自己的理解,尽量捋顺思路后,自己动手实现代码,看看有哪些坑点。多说无益,自己动手吧!

创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值