算法(一)双指针算法

目录

算法介绍

对撞指针

对撞指针 例题1 盛最多水的容器

题解思想

对撞指针 例题2 有效三角形个数

 题解思想

对撞指针 例题3 三数之和

题解思想

快慢指针

 快慢指针 例题1 移动零

题解思想

快慢指针 例题2 复写零

题解思想

快慢指针 例题3 快乐数

 题解思想


算法介绍

许多题目的最优解,往往都是对暴力解法的优化

有序数组两大常用算法:双指针、二分  (能用双指针,就别用二分)

双指针算法实际上是用数组下标充当指针,对数组中的元素进行操作

题目要求一般是对某个数组进行划分 / 操作数组中的元素。

常见的双指针题目形式有两种:对撞指针,快慢指针

对撞指针

对撞指针又称左右指针,一般用于顺序结构。

对撞指针一般是两个指针,一个从最左端开始,一个从最右端开始,逐渐向中间移动。

对撞指针的终止条件:一般是两个指针相遇 / 错开(也可能在循环内部找到结果,直接跳出循环) 

即 left == right (两个指针指向同一位置)/ left > right(两个指针错开) 

对撞指针 例题1 盛最多水的容器

题目描述
题目示例

 AC代码 

class Solution {
public:
    int Min(int x, int y)
    {
        if(x > y)
        {
            return y;
        }
        else
        {
            return x;
        }
    }
    int maxArea(vector<int>& height) 
    {
        // 利用单调性 + 双指针来实现
        int left = 0;
        int right = height.size() - 1;
        int max = Min(height[right], height[left]) * (right - left);  // 最大盛水体积
        int value = Min(height[right], height[left]) * (right - left);  // 每移动一次计算一次体积
        while(left != right)
        {
            // 1、小的那边移动(因为左右指针内部的元素,无论比小的那边大/小/相等,容积总是减少的)
            if(height[left] < height[right])
            {
                left++;
                value = Min(height[right], height[left]) * (right - left);
                if(value > max)
                {
                    // 2、大于max,就更新max
                    max = value;
                }
            }
            else
            {
                right--;
                value = Min(height[right], height[left]) * (right - left);
                if(value > max)
                {
                    // 2、大于max,就更新max
                    max = value;
                }
            }
        }
        return max;
    }
};

题解思想

设两个指针 left right 分别指向容器的左右两个端点,此时容器的容积为:

v = (right - left) * min( height[right], height[left])

容器的左边界为 height[left] ,右边界为 height[right]

为方便叙述,假设「左边边界」小于「右边边界」

如果此时我们固定⼀个边界,改变另⼀个边界,水的容积会有如下变化形式:

1、容器的宽度⼀定变小。

2、由于左边界较小,决定了水的高度。如果改变左边界,新的水面高度不确定,但是⼀定不会超

过右边的柱子高度,因此容器的容积可能会增大。

3、如果改变右边界,则无论右边界移动到哪儿,新的水面的高度⼀定不会超过左边界,也就是不会 超过现在的水面高度,但是由于容器的宽度减小,因此容器容积一定变小。

由此可得,左边界和其余边界的组合情况都可以舍去。

所以可以 left++ 跳过这个边界,继续去判断下⼀个左右边界。

当不断重复上述过程,每次都可以舍去大量不必要的枚举过程,直到 left right 相遇。期间产生的所有的容积里面的最大值,即是最终答案。

对撞指针 例题2 有效三角形个数

题目描述

AC代码

 题解思想

先把数组进行排序,就可以只凭借a + b > c判断三角形了,固定第三条边,然后对撞指针依次判断是否是有效三角形

对撞指针 例题3 三数之和

 AC代码 

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) 
    {
        vector <vector<int>> res;   // 最终返回结果,要做到不重+不漏

        // 1、排序
        sort(nums.begin(), nums.end());

        // 2、固定一个数,剩下的区间内采用对撞指针
        int third = 0;  // 固定一个数所在的位置
        while (third < nums.size()) 
        {
            int left = third + 1;
            int right = nums.size() - 1;
            while (left < right) 
            {
                if (nums[left] + nums[right] > -nums[third]) 
                {
                    right--;
                } 
                else if (nums[left] + nums[right] < -nums[third]) 
                {
                    left++;
                } 
                else 
                {
                    res.push_back({nums[left], nums[right], nums[third]});
                    // 确保不漏:一次结果正确后接着往后走
                    left++;
                    right--;

                    // 去重1:如果left的数和right的数重复,不用再算了(同时还要防越界)
                    while(left < right && nums[left - 1] == nums[left])
                    {
                        left++;
                    }
                    while(left < right && nums[right + 1] == nums[right])
                    {
                        right--;
                    }
                }
            }
            third++;
            // 去重2:如果出现重复的third执行一次即可
            while(third < nums.size() && nums[third - 1] == nums[third])
            {
                third++;
            }
        }
        return res;
    }
};

题解思想

本质是对暴力枚举进行优化

1、把数组先排序,然后固定一个数 count 。

2、在这个数后面的区间内,使用对撞指针快速找到两个数之和等于 -count 的即可。

但是要注意的是,这道题里面需要有 去重 + 不漏 操作

1、找到⼀个结果之后, left right 指针要「跳过重复」的元素;

2、当使用完一次对撞指针算法之后,固定的 count 也要「跳过重复」的元素。

3、确保不漏:一次结果正确后接着往后走

快慢指针

快慢指针的基本思想就是使用两个移动速度不同的指针在数组 / 链表等序列上移动。

这种方法在处理环形链表 / 数组时非常有用,当然也不单单是这两类,只要问题出现循环往复的情况时,均可考虑使用快慢指针思想。 

快慢指针最常见的实现方式

在一次循环中,慢的指针每次向后移动一位,快指针一次移动两位,实现一个快一个慢 

 快慢指针 例题1 移动零

题目描述

AC代码

题解思想

在本题目中,我们可以使用两个指针 dest 和 cur。cur 负责遍历数组;dest 则是一个标记,dest左边的元素都是非零元素,右边都是 0 。

因为 cur 负责遍历数组,所以初始化为0;而 dest 因为是标记,左边(包括其自己)必须都是非零元素,故为了防止数组第一个就是0,我们把 dest 初始化为 -1 。

然后 cur 遍历数组;遇到非零元素,dest 后移一位,与 cur 指向元素进行交换,直至 cur 遍历结束。这样就可以满足dest左边(包括其自己)都是非零元素,右边都是零元素,满足题目条件

(PS:本质上也利用了快排的思想,即数组划分区间 + 数组分两块)
 

快慢指针 例题2 复写零

题目描述

 AC代码

class Solution {
public:
    void duplicateZeros(vector<int>& arr) 
    {
        // 1、先找到复写后数组的最后一个数是哪一个(同样是一个双指针算法)
        int cur = 0;    // 指向复写后数组的最后一个数
        int dest = -1;  // 用来标记,当这个指针到原数组末尾时,cur指向的位置一定是复写后数组的最后一个数
        while(cur < arr.size())
        {
            if(arr[cur] != 0)
            {
                dest++;
            }
            else
            {
                dest+=2;
            }
            if(dest >= arr.size() - 1)
            {
                break;
            }
            cur++;
        }

        // 如果cur指向复写后数组的最后一个数是0,dest在最后一步会+=2,此时会越界,所以需要处理
        if(dest == arr.size())
        {
            arr[arr.size() - 1] = 0;
            cur--;
            dest-=2;
        }

        // 2、因为从前往后遍历会覆盖元素,所以从后往前遍历
        while(cur >= 0)
        {
            if(arr[cur] != 0)
            {
                arr[dest] = arr[cur];
                dest--;
            }
            else
            {
                arr[dest] = 0;
                arr[dest-1] = 0;
                dest -=2;
            }
            cur--;
        }
    }
};

题解思想

如果从前向后进行原地复写操作的话,由于 出现 0 会写两次,导致没有复写的数被覆盖掉。因此我们选择从后往前的复写策略。

但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的大体流程分两

步:

1、先找到最后⼀个复写的数;

2、然后从后向前进行复写操作。

快慢指针 例题3 快乐数

题目描述

AC代码

 题解思想

根据鸽巢原理可以得知,在循环执行数据的时候,数据必然会陷入到一个循环中,此时就是可以使用快慢指针,快指针一次走两步,慢指针一次走一步,由于数据陷入循环,所以必然两个指针会相遇,然后可以根据相遇的情况判断是否是快乐数。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值