【算法百题】专题一_双指针


前言

本文开始我将记录我对基础基础算法预计十八个专题、百道例题的学习历程和心得。


题目:

001. 移动零_C++

分析

将一个数组中的所有0元素放到数组后面,明显的数组划分问题(分块问题),我们需要一个去遍历整个数组的指针(cur)和一个标记已处理元素中非0元素的最后一个位置的指针(dest),由此便用双指针法,这里我们用两个int类型来充当数组的指针去随机访问数组。

算法的主要特性的就是要保持算法运行过程中情况的普遍性,当前双指针算法中我们使用了两个指针将数组划分为了三个块(cur划分了已处理的元素和未处理的元素区间,dest进一步划分了已处理元素中的非0元素和0元素区间)[0,dest] [dest+1,cur-1] [cur+1,n]左闭右闭区间在这里插入图片描述
所以为了保持我们算法中任何时间的情况是普遍且不违法的,我们使dest指针开始时在下标-1的位置,这样非0区间便表示为[0,-1],表示当前没有非0区间的。cur值为0开始。

我们扫描数组的指针cur有两种情况:
1.遇到非0:将cur位置的元素交换与dest位置的下一个(一定是0元素)。然后将dest和cur位置++。
2.遇到0:不操作直接++.

class Solution {
public:
    void moveZeroes(vector<int>& nums) 
    {
        for(int dest=-1, cur=0;cur<nums.size();cur++)
        {
            if(nums[cur])
                swap(nums[++dest],nums[cur]);
        }
    }
};

002.复写零_C++

分析

独立分析:也是区块划分问题,首先肯定需要一个遍历数组的指针cur(开始位置0),但是根据题目当数组中有0存在的话,肯定会有在尾部的元素被忽略,所以就用另外一个指针dest(开始位置-1)先模拟实际指针跳变情况(当cur遍历到非0元素dest+1,当遍历到0元素dest+2),循环条件是dest<arr.size(),这样当循环结束时,cur位置以前便是所有需要遍历到的元素。
然后记录cur下标的位置然后重新重头开始遍历?可是这样移动的话当有0元素时对0复写就会改变后面元素位置,我们遍历查找有效元素位置的行为就没有意义了。
从cur位置向前遍历然后从尾开始覆盖数组?貌似可以,但是需要考虑尾部特殊的情况,并不是所有的0都会被复写成功的,例如:arr = [1,0,2,3,0,0,5,1],最后一个0元素因为位置不够没有被复写,需要特别考虑。

class Solution 
{
public:
    void duplicateZeros(vector<int>& arr) 
    {
        int cur=0,dest=-1,n=arr.size();
        while(cur<n)
        {
            if(arr[cur]) dest++;
            else dest+=2;
            if(dest>=n-1)  break;
            cur++;
        }
        if(dest==arr.size())
        {
            arr[--dest]=0;
            dest--;
            cur--;
        }
        while(cur>=0)
        {
            if(arr[cur])
            arr[dest--]=arr[cur--];
            else
            {
                arr[dest--]=0;
                arr[dest--]=0;
                cur--;
            }
        }
    }
};

003.快乐数_C++

分析

独立分析:有点类似水仙花数?拆开一个数n的每位(对10取余然后对10做商)每位都加到一个num变量中(循环),当每位数都取到之后判断num是否为1,如果为1则为快乐数,不为1则让num的值做n再次进入上述循环。这样好像和双指针没有什么关系啊?

本题最重要的是第二句话 (然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。)

  1. 这句话给了我们极大的提示,当n是快乐数时,最终算的结果是1,而将1替换为它每个位置上的数字的平方和后还是1,证明n是快乐数时最终也还是会无限循环的,而无限循环说明了我们运算过程中一定有环的存在。

  2. 在链表章节我们知道,要证明一个链表中有环的存在,我们可以使用快慢指针实现,当快慢两指针在某一时刻相遇,则它们一定相遇在链表的环之中,所以我们这里也可以用双指针(快慢指针)去找到我们环之中的一个元素,当该元素是1,则数n为快乐数。
    在这里插入图片描述
    在这里插入图片描述

  3. 我们这里也可以用双指针的吗?我们并没有一个个节点啊。双指针是一种思想 每一次运算我们都可以看做是一个节点。(如在上图中我们将fast指针的每次跳变视作是经过两次运算,而slow指针的跳变是经过一次的运算)(开始时fast和slow都指向2,它们经过跳变后fast->16,slow->4)。

  4. 为什么说第二句给了我们极大的提示呢?因为其实只要是整形值,一直重复运算,最终都会有环(无限循环),为什么? 鸽巢原理 :3只鸽子放入两个巢穴中,则至少有一个巢穴中鸽子数量大于等于2。我们的整形值最大不过2.1*10^9,总共10位数,我们取10位9经过运算得810,所以我们整形的最大值经过运算肯定小于810,我们干脆直接取810为一个任意整形经过运算后的最大值,则我们任意的整形经过运算后的值肯定在区间[1,810]中,但是如果该整形经过了811次运算,则总会有一个在区间[1,810]中的值被重复取到,然后进入环中(无限循环),所以我们任意整形值经过多次运算后都会有环。

  5. 当我们进入循环中如何判断该循环的结束条件,以返回对应的结果呢?

class Solution 
{
public:
    int count(int n)
    {
        int nums=0;
        while(n)
        {
            int t=n%10;
            nums+=t*t;
            n/=10;
        }
        return nums;
    }

    bool isHappy(int n) 
    {
        int fast=n,slow=n;
        do
        {
            fast=count(fast);
            fast=count(fast);//这里可以嵌套使用fast=count(count(fast));
            slow=count(slow);
        }while(fast!=slow);
        return slow == 1;
    }
};

004.盛最多水的容器_C++

分析

独立分析:先试试暴力,用双指针去让所有线两两匹配,过程中计算并记录最大能盛的水的两条线。测试结果正确,但是超出时间限制。

class Solution 
{
public:
    int count(vector<int>& height,int x,int y)
    {
        int tmpmaxcap=0;
        int base=y-x;
        int High=height[x];
        if(height[y]<High) High=height[y];
        return High*base;
    }

    int maxArea(vector<int>& height) 
    {
        int maxcap=0;
        int n=height.size();
        int i=0,j=0;
        while(i<n)
        {
            j=i+1;
            while(j<n)
            {
                if(maxcap<count(height,i,j))
                maxcap=count(height,i,j);
                j++;

            }
            i++;
        }
        return maxcap;
    }
};

这题可以根据它的规律优化,时间复杂度可以从O(n^2)变到O(n),我们装换到小区间思考问题:在一个区间[6,2,5,4]中,我们使体积V=高h*宽w,我们先算出以区间左右两边作为线的高度的体积V1,所以如果在该区间内部找其他体积,则我们的宽度w一定减小, 所以向找到更大的体积就需要更高的高度h,所以我们就舍弃两边中更小的那个值,组成新的区间去重新寻找更大的体积,如此便省略了很多用小高度去比较的过程,不过侃侃遍历一遍数组,时间复杂度为O(n)

class Solution 
{
public:
    int count(vector<int>& height,int x,int y)
    {
        int tmpmaxcap=0;
        int base=y-x;
        int High=height[x];
        if(height[y]<High) High=height[y];
        return High*base;
    }

    int maxArea(vector<int>& height) 
    {
        int maxcap=0;
        int n=height.size();
        int i=0,j=n-1;
        while(i!=j)
        {
            if(maxcap<count(height,i,j))
                maxcap=count(height,i,j);
            if(height[i]<height[j]) i++;
            else j--;
        }
        return maxcap;
    }
};

看解析好像就单单左右两双指针跳过较小值,其实这个思想是根据该题目的单调性质想出来的,只是使用了双指针,实际非常不好想。

005.有效三角形的个数_C++

分析

独立分析:如果暴力,我们需要保存三个值的位置,这样的话就是三层循环:时间复杂度为O(N^3)。

  1. 数学知识补充:要证明三个数是否能组成三角形,我们只需要证明较小的两个数相加是否大于最大的那个数即可(即a+b是否大于c)。
  2. 既然有以上思想,那么我们便很自然的想到一种优化暴力解法的方法:先对整个数组排顺序,我们便可以直接比较前面两个值的和是否大于后面一个值(因为排后面的值一定相对大),如果大于,则可以组成一个三角形。先排顺序对暴力解法的优化十分显著,从原先的O(N^3)变为O(NlogN+N^3)。在这里插入图片描述
  3. 除了暴力解法,我们还可以利用排好的顺序的单调性质和需要组成三角形的条件性质,优化出另外一种更好的解法,时间复杂度达到O(N^2)。我们放到一个小区间[2,2,3,4,5]中寻找规律,因为我们需要找的三元组中最大的那个数十分特殊,所以我们第一步①先定最大的数c(也就是从数组的尾开始定),c=5,②然后就要在前面[2,2,3,4]区间中找到a和b的值,③最后判断他们是否满足a+b>c(是否满足三角形形成)的条件。这里我们先以区间[2,2,3,4]两边的两个值分别做a和b,a=2,b=4,那么此时a+b的情况无非两种:满足条件的a+b>c,或者不满足条件的a+b<=c。这里便可以用顺序的特性:当区间最右边(最大)的值的和区间最左边(最小)相加大于另一个值c,那么该区间内的所有值和最右边(最大)的值相加都大于该值c。 于是便有区间[2,2,3,4]右下标3-左下标0=3个数可以和c组成三角形。在这里插入图片描述
class Solution 
{
public:
    int triangleNumber(vector<int>& nums) 
    {
        int n=nums.size();
        sort(nums.begin(),nums.begin()+n);
        int num=0;
        int left,right;
        int i=n-1;
        while(i>0)
        {
            left=0;
            right=i-1;
            if(nums[i]==0)
            {
                i--;
                continue;
            }
            while(left<right)
            {
                if(nums[left]+nums[right]>nums[i])
                {
                    num+=right-left;
                    right--;
                }
                else
                {
                    left++;
                }
            }
            i--;
        }
        return num;
    }
};

006.和为 s 的两个数_C++

分析

独立分析:还是优先想到双指针暴力算法求解,只要求返回一个满足条件的结果,时间复杂度O(N^2),好像十分简单?果然没那么简单,超时了。

class Solution 
{
public:
    vector<int> twoSum(vector<int>& price, int target) 
    {
        vector<int> ret;
        int n=price.size();
        int find=0;
        for(int i=0;i<n;i++)
        {
            for(int j=i+1;j<n;j++)
            {
                if(price[i]+price[j]==target)
                {
                    ret.push_back(price[i]);
                    ret.push_back(price[j]);
                    find=1;
                    break;
                }
            }
            if(find) break;
        }
        return ret;
    }
};

再分析:题目给了很重要的提示:升序数组,显然暴力求解没有使用升序的性质,根据经验我们可以使用双指针分别指向数组中最小(最左边),和最大(最右边)的数,将他们相加看是否等于目标值,因为数字的性质,我们只能用两个小的数字相加得到大数,所以我们还可以优化右边的指针,让它从小于目标值的位置开始,可以避免很多不必要的比较。

class Solution 
{
public:
    vector<int> twoSum(vector<int>& price, int target) 
    {
        vector<int> ret;
        int n =price.size();
        int right=n-1;
        while(price[right]>target) right--;
        int left=0;
        while(left<right)
        {
            if(price[left]+price[right]==target) break;
            if(price[left]+price[right]>target) right--;
            if(price[left]+price[right]<target) left++;
        }
        ret.push_back(price[left]);
        ret.push_back(price[right]);
        return ret;
    }
};

衍生小知识

当要用数组进行返回值,且只用返回两个值,有一个方便的语法可以使用:
在这里插入图片描述

007.三数之和_C++

分析

独立分析:这个题目是中级的题,首先暴力解法肯定不行,但我还是试试:题目并没有描述它是升序数组,那么先还是将它排升序,这样就为解题提供了更多的方向。无法实现,三层循环而且要求不能有重复,难道去二维数组中再查有没有重复?
新思路:这题有点类似之前有效三角形的题目,我们是否能排好序后定一个大的数(数组最右边)然后然后用双指针操作左边的区间,如果相加结果大了则right–,小了则left++。使时间复杂度提升为O(N^2)。
去重操作需要容器set。在这里插入图片描述

果然第二种解法就是之前有效三角题的思路,不过为了完成去重操作,还需要对重复的元素进行跳过。
在这里插入图片描述

  1. 排序后开始先选一个数a作为目标数,然后从该数后面的区间中找两个值相加等于-a,这样就得到了三个相加为0的数。
  2. 根据有序的性质,我们还可以对算法优化:因为有序,当我们的a为正数时,无法从它后面的区间找到两个相加为-a的数,所以我们的a一定是非负数的。
  3. 区间选值时去重的循环判断很重要,当有重复进入循环,去重后判断指针位置是否正确(left<right)。
class Solution 
{
public:
    vector<vector<int>> threeSum(vector<int>& nums) 
    {
        sort(nums.begin(),nums.end());
        int n=nums.size();
        int i=0;
        vector<vector<int>> ans;
        while(i<n-2)
        {
            if(nums[i]>0)
            {
                break;
            }
            int nagtarg=-1*nums[i];
            if(i!=0&&nums[i]==nums[i-1])
            {
                i++;
                continue;
            }
            int left=i+1,right=n-1;
            while(left<right)
            {
                int sum=nums[left]+nums[right];
                if(sum<nagtarg)
                {
                    left++;
                    continue;
                }
                else if(sum>nagtarg)
                {
                    right--;
                    continue;
                }
                else
                {
                    ans.push_back({nums[i],nums[left],nums[right]});
                    left++;
                    right--;
                    //if(left>right) break;
                    while (nums[left] == nums[left - 1])
                    {
                        left++;
                        if (left > right) break;
                    }
                    while (nums[right] == nums[right + 1])
                    {
                        right--;
                        if (left > right) break;
                    }
                }
            }
            i++;
        }
        return ans;
    }
};

008.四数之和_C++

分析

独立分析:暴力枚举明显不是明智之举,枚举四位时间复杂度到达O(N^4)。在这里插入图片描述
如果用三数之和题目的思路:用排顺序数组然后定数然后双指针找两个符合条件的值:这次定两个数a、b,然后从b后的区间找两个符合的值。不知能否实现其代码,如实现时间复杂度预计会到达O(N^3),时间复杂度不算优,是否超时也未知。

思路大致没错,就是三数之和的思路,但不是定两个数用双指针在后面区间找符合,还是定一个数a,对a后面的区间套用三数之和的思路找到三个数的和等于target-a,时间复杂度同样是O(N^3),定两个数的可行性未知,思路的递进性也可以帮助更快解题和编写代码。在这里插入图片描述
在这里插入图片描述

class Solution 
{
public:
    vector<vector<int>> fourSum(vector<int>& nums, long long target) 
    {
        sort(nums.begin(),nums.end());
        vector<vector<int>> ret;
        int n=nums.size();
        int i=0;      //nums[i]即a
        while(i<n-3)
        {
            int j=i+1;//从区间【i+1,n-1】找三个和为target-a的数
            while(j<n-2)
            {
                long long deptarget=target-nums[i]-nums[j];
                int left=j+1,right=n-1;
                while(left<right)
                {
                    long long sum=nums[left]+nums[right];
                    if(sum>deptarget) right--;
                    else if(sum<deptarget) left++;
                    else
                    {
                        ret.push_back({nums[i],nums[j],nums[left],nums[right]});
                        left++;
                        right--;
                        while(left<right&&nums[left]==nums[left-1]) left++;
                        while(left<right&&nums[right]==nums[right+1]) right--;
                    }
                }
                j++;
                while(j>0&&nums[j]==nums[j-1]&& j < n - 2)
                {
                    j++;
                }
            }
            i++;
            while(i>0&&nums[i]==nums[i-1]&& i < n - 3)
            {
                i++;
            }
        }
        return ret;
    }
};

注意:当改变定值i/j并去重时,也需要小心越界。

总结

双指针题目。


本文完全是作者对个人算法经验的记录和累计,顺便进行知识分享,有任何错误请评论指点:)。

  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值