优选算法《双指针》

在学习了C/C++的基础知识之后接下来我们就可以来系统的学习相关的算法了,这在之后的笔试、面试或竞赛 都是必须要掌握的;在这些算法中我们先来了解的是一些非常经典且较为常用的算法,在此也就是优选出来的算法,接下来在每一篇章中我们都会来学习一种优选算法,并且在了解了算法原理之后接下来会通过几道算法题来巩固相应的算法原理。在每道算法题的讲解中都会通过题目解析------算法原理讲解------代码实现三步来带你完全吃透每道算法题,相信通过这一系列算法专题的学习,你的算法以及代码能力会有质的飞跃。接下来就开始本篇双指针专题算法的学习吧!!!


1.双指针算法

在之前数据结构链表和顺序表的学习当中我们就已经使用过了双指针的算法,就例如在删除数组当中的重复元素、判断一个链表是否为环、带环链表找出入环位置、找出链表的中间节点等算法题中我们就已经使用到双指针的算法思想,那么双指针的算法思想具体是什么呢?接下来就来详细的了解看看

常见的双指针有两种形式,一种是对撞指针,⼀种是左右指针
对撞指针:一般用于顺序结构中,也称左右指针。
• 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
• 对撞指针的终止条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
◦ left == right (两个指针指向同⼀个位置)
◦ left > right (两个指针错开)
快慢指针:又称为龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动。
这种方法对于处理环形链表或数组非常有用。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使用快慢指针的思想。
快慢指针的实现方式有很多种,最常用的⼀种就是:
• 在⼀次循环中,每次让慢的指针向后移动⼀位,而快的指针往后移动两位,实现⼀快⼀慢。

2.双指针在算法题内的使用

那么在了解的双指针的算法原理之后接下来就通过算法题来强化算法的使用

2.1 移动零

283. 移动零 - 力扣(LeetCode)

题目解析

在此该算法题题目还是较为简单的,通过以上的题目描述就可以看出这道题要我们实现的是将数组内的所有零都移动到非零元素之后并且还要保持所有非零元素的相对位置不变

例如以下示例:

要把上面的示例1当中的零移动到所有非零元素;并且要保持原来的非零元素1、3、12的相对位置不改变,那么移动了之后数组就变为以下形式:

[1,3,12,0,0]

算法原理讲解

以上算法题其实是属于数组分块类型的算法题,在数组划分、分块的算法题中典型的特征就是题目会给我们一系列的要求将数组划分为几个区间,在以上我们要解决的算法题中就是要划分为元素为非零和零两个区间

该算法题其实就是非常适合使用双指针来解决,在此在解决数组问题时使用双指针时其实指针不是真正和链表内一样表示指向元素的指针,在数组是使用数组下标来充当指针

接下来就是使用双指针来解决以上算法题,以下是算法思路:

在此创建两个数组下标分别是cur和dest作用分别是从左往右遍历数组、使得dest为已处理区间内的非零的最后一个元素。在此过程中数组就可以划分为以下的三个区间

通过以上的图示就可以看出处理过程分为3个区间,分别是[0,dest]、[dest+1,src-1]、[src,n-1](n表示数组的长度)。最终当src遍历到数组的末尾时就只剩下两个区间[0,dest]、[dest+1,n-1]

接下来就来通过以上的示例1来具体分析dest和src的具体过程:

首先由于一开始不确定数组的第一个元素是否为非零元素,因此一开始将dest赋值为-1;src赋值为0,这样就能确保在遍历过程中不会有遗漏

之后使用双指针的流程图如下所示:

通过以上的示例就可以看出在使用双指针过程中流程:
1.当cur下标位置的元素值为0是只让cur++

2.当cur下标位置的元素不为零时就先让dest++,之后再交换dest和src位置的元素

代码实现

cpp 复制代码
class Solution {
public:
    void moveZeroes(vector<int>& nums) 
    {
       int dest=-1;
       for(int src=0;src<nums.size();src++)
       {
           //当src下标位置的元素值不为0时就交换dest和src位置的元素值
            if(nums[src]!=0 && ++dest!=src)
            {
                swap(nums[src],nums[dest]);
            }
       }
        
    }
};

2.2 复写零

1089. 复写零 - 力扣(LeetCode)

题目解析

首先通过以上算法题描述就可以得出该算法题要我们实现的是将一个数组按照元素为0时将对应元素值复写两遍;将非0的元素值复写1遍

接下来就通过以上的示例1来理解复写这个过程具体是如何进行的

算法原理讲解

在该算法题中如果是能创建一个新的数组那么要实现以上复写过程就是非常简单的,就只需要开一个大小和原数组一样大的数组,创建两个指针分别指向原数组和新数组的首元素下标,之后遍历原数组;当此时指向原数组下标的指针指向的元素为0时就将指向新数组的下标指针以及之后的元素值都赋值为0;反之就只让指向新数组指针的值赋值为此时指向原数组下标的值。

但是在该算法题中我们不能使用这种算法来解决,这是因为在该算法题中要求了我们要就地实现复写,那么就不能通过申请新的内存空间来创建新的数组

这时你可能就又会想到使用双指针来解决这道题,直接创建两个指针cur和dest,让cur遍历数组,之后通过dest来实现复写;但cur指向的元素值不为0时就只将dest指向的元素值赋值为cur指向元素的值,反之就将dest指向元素赋值为0并且将之后一位的元素值也赋值为0,完成操作之后再让dest++。

这样看起来就确实是能解决该算法题了,但是这样其实存在在复写时会会出现dest指针步长长与cur,这时就会再复写过程中cur还未遍历完数组,数组之后的元素值就改变了

例如以上的示例使用这种算法过程如下所示:

那么这是我们就要想了,双指针从正向遍历原数组会造成原数组的复写过程发生异常,有什么办法能避免这种问题呢?

正着不行,反着试试呢?如果我们能在原数组当中找到最后一个需要复写的数,之后再从这个数开始遍历原数组,这样不就能实现在原数组上实现复写的操作了吗

例如以上的示例使用这种算法过程如下所示:

那么接下来的问题就是如何找到原数组当中最后一个要复写的数了

其实要解决这个问题很简单就只需要一开始创建两个指针cur和dest都指向数组的第一个元素,之后让dest遍历数组,当遍历到非零元素时cur向后移动一位;当遍历到元素为零时cur向后移动两位,最后当dest遍历完原数组时cur指向的就是最后一位要复写的元素

例如以上的示例使用这种算法过程如下所示:

那么找到了最后一个要复写的元素之后就是从cur元素开始对dest位置进行复写

从 cur 位置开始往前遍历原数组,依次还原出复写后的结果数组:
i. 判断 cur 位置的值:

  1. 如果是 0 : dest 以及 dest - 1 位置修改成 0 , dest -= 2 ;
  2. 如果⾮零: dest 位置修改成 0 , dest -= 1 ;
    ii. cur-- ,复写下⼀个位置。

但其实在第一步找到原数组当中最后一个要复写的数,以及第二步从后向前复写之中还要有一个1.5步就是对最后一个复写的数时0这种情况进行处理,因为这种情况下最后一次复写就会dest指向arr.size(),这时步骤二中将dest位置的值修改为0就会出现数组的越界访问。因此对应这种特殊情况就要进行以下处理:

  1. n - 1 位置的值修改成 0 ;2. cur 向移动⼀步;3. dest 向前移动两步。

因此要解决这道算法题就算法流程如下所示:

a. 初始化两个指针 cur = 0 , dest = -1 ;
b. 找到最后⼀个复写的数:

i. 当 cur < n 的时候,⼀直执⾏下⾯循环:
• 判断 cur 位置的元素:
◦ 如果是 0 的话, dest 往后移动两位;
◦ 否则, dest 往后移动⼀位。
• 判断 dest 时候已经到结束位置,如果结束就终⽌循环;
• 如果没有结束, cur++ ,继续判断。
c. 判断 dest 是否越界到 n 的位置:
i. 如果越界,执行下⾯三步:

  1. n - 1 位置的值修改成 0 ;
  2. cur 向移动⼀步;
  3. dest 向前移动两步。
    d. 从 cur 位置开始往前遍历原数组,依次还原出复写后的结果数组:
    i. 判断 cur 位置的值:
  4. 如果是 0 : dest 以及 dest - 1 位置修改成 0 , dest -= 2 ;
  5. 如果⾮零: dest 位置修改成 0 , dest -= 1 ;
    ii. cur-- ,复写下⼀个位置。

代码实现

cpp 复制代码
class Solution {
public:
    void duplicateZeros(vector<int>& arr) 
    {
        //创建两个指针
        int cur=0,dest=-1;
        //找到最后一个复写的数下标
        while(cur<arr.size())
        {
            if(arr[cur]==0)
            {
                dest+=2;
            }
            else
            {
                dest++;
            }
            //当dest已经到数组最后一位或者已经越界就跳出循环
            if(dest>=arr.size()-1)
            {
                break;
            }
            cur++;
        }
        //处理最后一个复写的数是0的情况
        if(dest==arr.size())
        {
            arr[dest-1]=0;
            cur--;
            dest-=2;
        }
        //进行复写操作
        while(cur>=0)
        {
            if(arr[cur]==0)
            {
                arr[dest--]=0;
                arr[dest--]=0;
                cur--;
            }
            else
            {
                arr[dest--]=arr[cur--];
            }
        }
      
    }
};

2.3 快乐数

202. 快乐数 - 力扣(LeetCode)

题目解析

通过以上的题目描述就可以看出该算法题要我们实现的是判断给定的一个整数是不是快乐数,那么快乐数是什么呢,在以上题目中进行了解释,将这个数不断的替换成其各个位置的平方之和,最后循环时一直为1时就表明这个数是快乐数

例如以下示例:

算法原理讲解

要解决以上这道算法题就我们就要思考什么方法能实现在对指定的数当中在转换过程中得出最后循环的入循环时的数值是否为0,是0就表明该数是快乐数;否则就不是快乐数

其实要解决该过程就可以用到双指针,只不过这和之前我们使用双指针的场景不太一样,之前使用双指针都还是在链表或者数组的情况下。而此时的指针指向的是一个数,各个数通过之间的转换关系就链接到了一起,在此就需要先创建一个快指针fast和一个慢指针slow之后让快指针每次走两步;慢指针每次走一步;最后当快指针和慢指针相遇时就说明当前的数就是入环的数是否为0,为0就表明原来的数是快乐数;否则就不是

代码实现

cpp 复制代码
class Solution {
public:
    //得到数各个位数平方之和
    int getsum(int n)
    {
        int sum=0;
        while(n>0)
        {
            sum+=(n%10)*(n%10);
            n=n/10;
        }
        return sum;

    }

    bool isHappy(int n) 
    {
        //设置快慢指针
        int fast=getsum(getsum(n));
        int slow=getsum(n);

        //判断快慢指针是否相遇
        while(slow!=fast)
        {
            //快指针每次走两步,慢指针每次走一步
            fast=getsum(getsum(fast));
            slow=getsum(slow);

        }
        return slow==1?true:false;
        
    }
};

2.4 盛水最多的容器

11. 盛最多水的容器 - 力扣(LeetCode)

题目解析

通过以以上的题目描述就可以知道该算法题要我们要找出能盛最多水的容器,由于盛水的容器能装水的最高高度是由低的一边决定的,所以容器盛水的体积就是两边界的距离乘高度低的一边,要找出能盛最多水的容器也就是要找出以上乘积的最大值

在以上的示例1当中当左边界数组下标为1时,右边界数组下标为8时此时容器的容积就为7*7=49,之后的选择的容器都不会比这时的容器容积大,因此这种情况下最大的盛水值就为49

算法原理讲解

在此要解决该算法题首先我们先来想如何来暴力穷举,之后再对暴力解法进行优化

注:在学习算法过程中暴力解法也是非常重要的,毕竟在刚看到一道算法题的时候我们一般没办法一开始就想到最优的解法,这时就需要先思考如何使用暴力解法解决;之后再使用数学以及算法的思维对暴力解法进行优化。并且在解决算法题当中画图推导是非常重要的,不要只一直盯着题目看之后直接写代码,这样写出来的代码有可能会和我们实际想的会有很大的偏差,因此算法的推导过程是非常重要的。

那么接下来我们就对以上的示例1进行暴力穷举

首先我们定义两个指针都指向数组下标为0的位置,之后让right往后移动一位;之后一直移动直到超出数组的范围且每次移动right都统计此时的容器容量,之后当right移动到数组末尾之后就将left向后移动一位,再将right移动到left的位置,最后当left移动到数组的最后一个有效位置就停止穷举

过程图如下所示:

通过以上我们就了解了该算法题使用暴力穷举的方法是如何实现的,但是通过以上的示例视图就可以看出使用穷举就需要将很多组数据都列出来,这种方式当数据量很大时算法的运行效率就会很低

在该算法题的最大数据情况下一定会运行超时

那么暴力解法有什么可以优化的地方呢?

我们还是通过以上的示例来发现规律,如果我将left指针还是指向数组的首个元素,但是将right指向数组的最后一个元素,这时就得到容器底最宽的容器,这时计算此时容器的体积。之后如果我们将right往左移动就会发现以下的规律,由于容器的容积=两边的位置之差*两边高度低的那边

这时由于right在左移过程中两边的位置之差一定是在减小的,两边高度低的那边可能保持不变或者下降,那么就可以得出right在往左移动过程中容器的容积相比原来的容积一定是在减小的。因此为了避免这些无用的计算,那么就可以将left直接往右移动一位,这就不会进行无用的计算。

因此当left再往右移动之后就再比较left位置和right位置的值,按照以上我们得出来规律就需要将两个指向指向的数组值小的那一个指针移动一位,并且在移动之前记录此时的容器容积是否大于之前的容器容积;是的话就将容器的容积赋值为当前的值。之后一直重复以上的操作直到两指针left和right相遇就停止比较。

以上的算法在示例1当中的流程图就如下所示:

这时就可以看出相比暴力穷举数据的比较就少了很多,算法的时间算法度由于只遍历了一次数组就为O(N)。

代码实现

cpp 复制代码
class Solution {
public:
    int maxArea(vector<int>& height) 
    {
        //sum统计容器的最大容积
        int left=0,right=height.size()-1,n=height.size(),sum=0;
        //当左边界小于右边界就进行比较
        while(left<right)
        {
            //记录左右边界当中小高度的值
            int h=min(height[left],height[right]);
            //记录当前容器的底宽度
            int len=right-left;
            //比较当前的容器大小是否比之前的容器最大值大,是就将容器的最大值替换为当前的值
            sum=max(h*len,sum);
            //比较两边界的高度
            if(height[left]<height[right])
            left++;
            else
            right--;
        }
        return sum;
        
    }
};

2.5 有效三角形的个数

611. 有效三角形的个数 - 力扣(LeetCode)

题目解析

通过以上的题目描述就可以看出该算法题要我们求得是对给定得数组数值进行分析,得到数组当中可以组成三角形三边得最大三元组个数
在此每一个三元组就表示符合组成三角形的三条边的数值

注:我们要知道的是三个数值能组成三角形要求任意两边之和大于第三边,任意两边只差小于第三边。

算法原理讲解

在此要解决该算法题我们一开始能想到的就是使用暴力枚举的方法来求解,将给定的数组中的所有三元组都枚举出来之后再将符合组成三角形的三元组统计出来,最后返回统计的个数即可

但是再使用暴力枚举时我们一开始在得到每个三元组之后都要将三元组内进行判断任意的两个个数是否大于第三个数;任意的两个数是否小于第三个数。在此就需要再进行三组数据的判断,再结合之前的暴力枚举综合的时间复杂度就为O(3N^3),那么再结合这道题的数据范围这样写一定会超时

那么再暴力枚举的方式下有什么可以优化的地方呢?
在此我们先要知道的是在一个三元组当中如果数据是升序的,那么只要符合前两个数大于第三个数那么这个三元组就能构成三角形。使用在使用暴力枚举之前可以先将原给定的数组进行排序这样时间复杂度就能减少到O(N^3)

那么除了以上将数组排序还有什么可以优化的地方呢?

在此其实我们就可以根据单调性来对暴力求解进行优化,首先是我们先将原数组排为升序之后接下来我们可以创建变量i从后往前遍历原数组,再定义两个指针变量left和right一开始分别指向数组的首元素和最后一个元素之前的元素,这时通过比较left和right两个元素之和和i的大小关系。在此就可以得到一些规律

来看以下示例:

在以上我们先将数组排为升序之后接下来就创建相应的指针变量

这时就会发现left指向的变量和right指向的变量之和大于i指向的变量,这时这三个变量组成的三元组就可以构成三角形,这时根据单调性就可以得出left到right前一位的元素都可以和right指向的元素以及i指向的元素构成三角形。因此这种情况下就只需要让总的可以组成三角形的三元组个数加等right-left的值即可;这样就不再需要进行这段区间之间的遍历。之后再让right--再进行判断,如果出现left指向的变量和right指向的变量之和小于等于i指向的变量之后根据单调性就需要将left++。最后当left不小于right时就将i--进行下一组的判断;重复以上操作直到i小于2时就停止,这时是因为只有两个元素就无法构成三元组。

以上示例的过程图就如下所示:

代码实现

暴力解法:

cpp 复制代码
class Solution {
public:
    int triangleNumber(vector<int>& nums) 
    {
        int n=nums.size(),sum=0;
        //使得原数组变为升序
        sort(nums.begin(),nums.end());
        //暴力枚举
        for(int i=0;i<n;i++)
        {
            for(int j=i+1;j<n;j++)
            {
                for(int k=j+1;k<n;k++)
                {

                    //判断三元组内的元素是否符合构成三角形的条件
                    if(nums[i]+nums[j]>nums[k])
                    sum++;
                }
            }
        }
        return sum;
        
    }
};

使用双指针优化:

cpp 复制代码
class Solution {
public:
    int triangleNumber(vector<int>& nums) 
    {
        //使得数组变为升序
        sort(nums.begin(),nums.end());
        int n=nums.size(),sum=0;
        //遍历数组
        for(int i=2;i<n;i++)
        {
            int left=0,right=i-1;
            //使用双指针筛选
            while(left<right)
            {
                if(nums[left]+nums[right]>nums[i])
                {
                    sum+=right-left;
                    right--;
                }
                else
                {
                    left++;
                }
            }
        }
        return sum;
        
    }
};

在此使用双指针优化之后的代码时间复杂度为O(N^2)相比原来暴力枚举的方法效率提升了非常多。

2.6 和为 s 的两个数字

LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)

题目解析

通过以上的题目描述就可以看出该算法题要我们实现的是从给定的数组当中找出满足选定数组当中的两个元素和为target的二元组,最后将满足条件的二元组返回即可

算法原理讲解

要解决该算法题我们一开始想到的就是直接使用暴力枚举的方式将所有的二元组列出来之后再将满足和为targe的二元组返回即可,这样的实现出的代码时间复杂度就为O(N^2),那么对于暴力枚举的方法可以使用什么方式来优化呢?

在此其实就可以使用到双指针来优化暴力求解的过程,由于题目当中给定的数组是升序的,那么一开始就可以创建两个指针变量left和right;一开始两个指针分别指向数组的首个元素和最后一个元素,这时就判断left指向的元素和right指向的元素之和和targe之间的关系,如果比targe大那就将left++;比targe小就将right--,直到找到相等就停止

以上示例2按照我们的优化之后算法流程图如下所示:

代码实现

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& price, int target) 
    {     
        int n=price.size();
        int left=0,right=n-1;
        while(left<right)
        {
            if(price[left]+price[right]>target)
            right--;
            else if(price[left]+price[right]<target)
            left++;
            //当和为targe时
            else
            return {price[left],price[right]};
        }
        //使得所有情况东有返回值
        return {-1,-1};
        
    }
};

以上代码只遍历一次数组因此时间复杂度为O(N),相比暴力枚举的O(N^2)效率提升很大

2.7 三数之和

15. 三数之和 - 力扣(LeetCode)

题目解析

通过以上的题目描述就可以看出该算法题要我们从给点的数组当中找出所有满足和为0的三元组,并且在同一个三元组当中数组同个元素只能选一次,就例如以上题目示例2当中我们不能将数组的首元素选择三次。最后我们要将所有满足条件的三元组返回。

算法原理讲解

在解决这道算法题时我们一开始能想到的解法一般还是暴力枚举,实现出来的代码也就是三层的循环嵌套,但这种方式实现的代码时间复杂度为O(N^3),这种效率在这道题给定的数据范围内一定会超时,那么这时我们就要思考在暴力求解中能有什么优化的地方

在此我们直接要得到三个数组元素的和为0较为困难,但假设使用指针cur遍历数组;如果能先找出两个数组元素和为a[cur]那就简单多了。在上一道算法题当中我们不就正好解决了两数之和为指定值的问题吗?所以在这道三数和指定值的算法题我们就可以使用之前两数和为指定的算法思想再结合遍历数组就能解决了。不过在此有所不同的是这题当中找到left和right还要继续移动

因此简单来说就以上算法的步骤如下所示:
i. 先排序;
ii. 然后固定⼀个数 a :
iii. 在这个数后面的区间内,使用「双指针算法」快速找到两个数之和等于 -a 即可。

但在这道算法题当中还有一个要注意的点就是最后的三元组不能有重复的。这就使得我们要在选取出之后进行去重操作,也就是找到⼀个结果之后, left 和 right 指针要「跳过重复」的元素;当使用完⼀次双指针算法之后,固定的 a 也要「跳过重复」的元素。

例如以上示例1使用优化之后的算法过程图如下所示:
先将原数组排为升序

之后再进行以下操作

代码实现

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) 
    {
        //将原数组排为升序
        sort(nums.begin(),nums.end());
        //创建元素为vector<int>的vector对象
        vector<vector<int>> v;
        int n=nums.size();
        //遍历数组
        for(int i=0;i<n-2;)
        {
            //当i指向的元素都为正数时就不可能再形成和为0的三元组,这时就跳出循环
            if(nums[i]>0)break;
            //将遍历数组时i下标的值得相反数定义为sum
            int left=i+1,right=n-1,sum=-nums[i];       
            //从left到right区间内找两元素和为sum
            while(left<right)
            {
                //当left和right指向的元素和大于sum时right向左移一位
                if(nums[left]+nums[right]>sum)
                right--;
                //当left和right指向的元素和小于sum时left向右移一位
                else if(nums[left]+nums[right]<sum)
                left++;
                
                else
                {
                    //当left和right指向的元素和等于sum是向v内插入三元组
                    //之后将left向右移动一位;right向左移一位
                    v.push_back({nums[left],nums[right],nums[i]});
                    left++,right--;
                    // left跳过重复的元素
                    while(left<right && nums[left]==nums[left-1])left++;
                    //right跳过重复的元素
                    while(left<right && nums[right]==nums[right+1]) right--;
                }
            }
            //将i向右移动一位
            i++;
            //i跳过重复的元素
            while(i<n && nums[i]==nums[i-1])i++;  

        }
        return v;
        
    }
};

在以上代码中我们的for循环括号内写成了(int i=0;i<n-2;)这里面没有调整部分,这是为了处理在将i跳过重复元素时避免将i++直接写在循环括号后会造成处理完重复元素之后i会多加一。所以就直接将该循环的调整部分i++写在i跳过重复元素这步之前

以上代码时间复杂度为O(N^2),相比暴力O(n^3)的效率提升是很大的

2.8 四数之和

18. 四数之和 - 力扣(LeetCode)

题目解析

通过以上的题目描述就可以看出这道算法题要我们实现的是从给定的数组当中选取四个数组元素的和为targe,最后将所有满足的四元组返回,并且要将所有的四元组去重

算法原理讲解

在该算法题当中如果使用暴力枚举要使用4层for循环,在这道题的范围下一定会超时,在此暴力枚举的具体过程和之前三数之和类似,在此就不细致讲解。接下来就来分析该如何基于暴力枚举进行算法的优化。

在此其实我们就可以根据我们之前解决三数之和的那道算法题的经验,我们先创建一个变量i来遍历数组,在这道算法题当中先可以将要求解四数之和的数转换为三数之和再加i指向的元素,之后三数之和再按照之前解决三数之和的方式。通过以上的过程这样就可以解决这道算法题。

因此算法过程简单来说就是以下形式:

**a. 依次固定⼀个数 a ;
b. 在这个数 a 的后面区间上,利用「三数之和」找到三个数,使这三个数的和等于 target

  • a 即可。**

并且在此除了和之前三数之和一样left、right和固定的数要去重外,最外层确定值a也要去重

代码实现

cpp 复制代码
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) 
    {
        //将原数组排升序
        sort(nums.begin(),nums.end());
        vector<vector<int>> v;
        int n=nums.size();
        //遍历数组,固定第一个数
        for(int i=0;i<n;)
        {
            //固定第二个数
            for(int j=i+1;j<n;)
            {
                int  count2=target-nums[i]-nums[j];
                int left=j+1,right=n-1;
                //找出两数之和为count2的二元组
                while(left<right)
                {
                    int count1=nums[left]+nums[right];
                    
                    if(count1>count2)
                    right--;
                    else if(count1<count2)
                    left++;
                    else
                    {
                        //将符合要求的四元组插入到v当中,
                        //之后left向右移动因为,right向左移动一位
                        v.push_back({nums[left],nums[right],nums[i],nums[j]});
                        left++,right--;
                        // left跳过重复的元素
                        while(left<right && nums[left]==nums[left-1])left++;
                        // right跳过重复的元素
                        while(left<right && nums[right]==nums[right+1])right--;

                    }
                              
                }
                j++;
                    // j跳过重复的元素
                    while(j<n && nums[j]==nums[j-1])j++;    
            }
            i++;
                 //i跳过重复的元素
                while(i<n && nums[i]==nums[i-1])i++;


        }
        return v;
    }
};

以上代码我们提交之后会在以下的测试用例下通不过,这是为什么呢?

根据编译报错位置就可以知道是在求count时int无法存储以上示例当中的值存储完整,因此要将count类型修改为long long

优化之后的代码如下所示:

cpp 复制代码
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) 
    {
        //将原数组排升序
        sort(nums.begin(),nums.end());
        vector<vector<int>> v;
        int n=nums.size();
        //遍历数组,固定第一个数
        for(int i=0;i<n;)
        {
            //固定第二个数
            for(int j=i+1;j<n;)
            {
                long long count2=(long long)target-nums[i]-nums[j];
                int left=j+1,right=n-1;
                //找出两数之和为count2的二元组
                while(left<right)
                {
                    int count1=nums[left]+nums[right];
                    
                    if(count1>count2)
                    right--;
                    else if(count1<count2)
                    left++;
                    else
                    {
                        //将符合要求的四元组插入到v当中
                        //之后left向右移动因为,right向左移动一位
                        v.push_back({nums[left],nums[right],nums[i],nums[j]});
                        left++,right--;
                        // left跳过重复的元素
                        while(left<right && nums[left]==nums[left-1])left++;
                        // right跳过重复的元素
                        while(left<right && nums[right]==nums[right+1])right--;

                    }
                              
                }
                j++;
                    // j跳过重复的元素
                    while(j<n && nums[j]==nums[j-1])j++;    
            }
            i++;
                 //i跳过重复的元素
                while(i<n && nums[i]==nums[i-1])i++;


        }
        return v;
    }
};

以上代码时间复杂度为O(N^3),相比暴力O(n^4)的效率提升是很大的

以上就是本篇的全部内容了,接下来还会带来其他的优选算法,未完待续......

相关推荐
yannan20190313几秒前
【数据结构】(Python)树状数组+离散化
开发语言·python·算法
Moring.3 分钟前
牛客周赛 Round 72 <字符串>
算法
飞飞-躺着更舒服10 分钟前
C/C++ 文件处理详解
c语言·c++·算法
南宫生14 分钟前
力扣-图论-14【算法学习day.64】
java·学习·算法·leetcode·图论
二闹44 分钟前
青训营试题算法解析十五
后端·算法
清风序来1 小时前
C++多态
算法
凡人的AI工具箱1 小时前
每天40分玩转Django:实操 Todo List应用
数据库·后端·python·算法·django
忘梓.2 小时前
解锁动态规划的奥秘:从零到精通的创新思维解析(1)
算法·动态规划
cdut_suye2 小时前
动态规划在斐波那契数列中的应用与优化
数据结构·c++·人工智能·python·算法·动态规划·热榜
WineMonk2 小时前
.NET C# 国密算法(SM算法)详细实现
算法·c#·.net