【优选算法 | 双指针】双指针大揭秘:如何用两根指针优化你的代码

#新星杯·14天创作挑战营·第10期#

在这里插入图片描述

算法相关知识点可以通过点击以下链接进行学习一起加油!

在本篇文章中,我们将深入探索双指针算法的奥秘。从基础概念到实际应用,带你全面了解如何利用两根指针高效解决各种编程问题。无论你是刚接触算法的新人,还是希望提升代码性能的老手,双指针都是你不可忽视的利器!

请添加图片描述

Alt

🌈个人主页:是店小二呀
🌈C/C++专栏:C语言\ C++
🌈初/高阶数据结构专栏: 初阶数据结构\ 高阶数据结构
🌈Linux专栏: Linux
🌈算法专栏:算法
🌈Mysql专栏:Mysql

🌈你可知:无人扶我青云志 我自踏雪至山巅 请添加图片描述

283.移动零[数组划分]

题目展示】:283.移动零

在这里插入图片描述

输入:[0, 1, 0, 3, 12]

输出:[1, 3, 12, 0, 0]

算法思路

这类问题可以分为数组划分或者叫数组分块,并且使用双指针算法。这里提供指针作用、具体步骤、部分设计,三个方面的解析。

在这里插入图片描述

1.指针作用

  • 【cur】:从左往右扫码数组,遍历数组
  • 【dest】:已处理的区间内,非零元素的最后一个位置

2.具体步骤:

  1. cur从前往后遍历的过程中:
  2. 遇到0元素】:cur++;
  3. 遇到非零元素】:swap(++des,cur); cur++;

3.区域划分:这里需要保证[0, dest]是非0,[dest + 1, cur - 1]是0这个设计。dest设置为-1使得[0, dest]一开始不存在。最后通过cur遍历通过中,使用swap函数,将数据进行划分。

代码展示】:

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

个人思考】:遇到数组分块等类似题目,可以借助双指针进行数组划分,通过swap交换将不需要的数据排除该区间

小扩展】:快排里面最核心的一步,也是通过tmp进行数组的划分

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

那么可以按照[0, tmp]、[tmp, cur -1]、[cur, n -1]来划分,代码是类似的


1089.复写零[遍历角度]

题目展示】:1089.复写零

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 输入:[1,0,2,3,0,4,5,0]
  • 输出:[1,0,0,2,3,0,0,4]

问题解析】:

1.从左到右遍历不行

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

cur需要判断的数据被dest覆盖,原因在于dest在cur之后进行了操作。如果是‘删除等于val值’这类题目中,dest始终保持在cur前面,因此不会出现数据被覆盖的情况。

2.转化角度

如果从左往右遍历会出现数据覆盖的情况,可以尝试从右往左进行覆盖,从结果的最后一个数字开始,按逆序遍历。

算法思路

步骤分为两个阶段

  1. 定位结果的最后一个元素
    可以使用双指针法遍历数组,此过程中无需修改数据,只需找到结果中的最后一个有效元素,并确定 destcur 应指向的位置。
  2. 从右往左进行覆盖
    在确定了结果末尾位置后,再从右向左逐步覆盖数据。

1.第一步:找到最后一个"复写"的数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过推导输入与输出元素的位置关系,我们发现 cur 指向最后一个有效元素(例如数字 4),而 dest 指向数组的末尾。如果保留原始的两个 0 元素,则 curdest 之间相差 2,这表明 0 元素的数量会影响 destcur 的移动步幅。

2.推导位置出现特殊情况

如果数组中不存在 0 元素,两个指针会同时向前移动。虽然既可以用 cur < n 也可以用 dest > n - 1 作为循环判断条件,但考虑到只有当 cur > n 时才能确保遍历完整个流程,其范围更广。因此,作为循环的终止条件,通常只需判断 dest > n - 1 来 break 循环即可。

3.第二步:移动数据

  • 遇到非零元素】:交换数据 arr[dest--] = arr[cur];

  • 遇到零元素】: 重复两次arr[dest--] = 0;

特殊情况处理

这里需要进行特殊处理:当 dest 达到 n 时,可能会导致数据覆盖,从而引发越界访问。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//2.特殊情况处理,处理完也是需要对位置进行移动的
        if(dest == n)
        {
            arr[n - 1] = 0;
            dest -= 2;
            cur--;
        }

在这里插入图片描述

代码展示】:

class Solution {
public:
    void duplicateZeros(vector<int>& arr) 
    {
        //1.先找到最后一个位置
        int cur = 0, dest = -1, n = arr.size();
        while(cur < n)
        {
            if(arr[cur] == 0) dest+=2;
            else dest++;
            if(dest >= n - 1) break;
            cur++;
        }
        
        //2.特殊情况处理,处理完也是需要对位置进行移动的
        if(dest == n)
        {
            arr[n - 1] = 0;
            dest -= 2;
            cur--;
        }

        //3.开始数据处理
        while(cur >= 0)
        {
            if(arr[cur]) arr[dest--] = arr[cur];

            if(arr[cur] == 0)
            {
                arr[dest--] = 0;
                arr[dest--] = 0;
            }

            cur--;
        }
    }
};

个人思考】:在需要判断和修改数组元素的问题中,通常会想到双指针方法。但若从左到右遍历,可能会导致数据覆盖,从而影响结果。对此,不妨尝试调整遍历方向,说不定会带来意想不到的优化效果。


202.快乐数[快慢指针]

题目展示】:202.快乐数

image-20240514185851149

示例 1:

  • 输入:n = 19
  • 输出:true

解释:

  • 12 + 92 = 82
  • 82 + 22 = 68
  • 62 + 82 = 100
  • 12 + 02 + 02 = 1

示例 2:

  • 输入:n = 2
  • 输出:false

算法思路

1.是否为闭环

如果题目中没有提示“重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1”,那么我们必须额外判断以下三种情况,以确保程序能够正确终止:

  • 情况一】:一直在 1中死循环,即1->1->1
  • 情况二】:在历史的数据中死循环,但始终变不到1
  • 情况三】:单路线不断变化新数字,不是死循环

2.闭环会限制变化的范围

因此,我们需要判断该数在变化过程中是否会形成闭环。形成闭环意味着至少会重复出现一次相同的数,此时数值变化的范围已被锁定。

3.证明鸽巢原理:

鸽巢原理:n个巢,n + 1个鸽,至少有一个巢,里面的鸽数大于1,必有一个重复。那么意味着,只需要确定了[1, n]范围,就说明到n + 1必有一个重复的。而这个最大的n,是可以通过一个最大数去推。

数据范围:1 <= n <= 231 - 1,选一个更大的数9999999999。通过变化的最大值9^2 * 10 = 810,那么变化的区间在[1, 810]之间。这里是通过最大数推导出可能的最大变化范围,但实际最大值 810 本身并不包含在内。

根据鸽巢原理,当一个数变化到811次之后,必然会形成一个循环。**当形成一个闭环时,可以使用我们的快慢指针解决。**因为1形成的闭环,里面全是1。

具体步骤】:

  • 当快慢指针相遇,相遇位置的值是1,那么这个数一定是快乐数

  • 当快慢指针相遇,相遇位置的值不是1,那么这个数不是快乐数

代码展示】:

class Solution 
{
public:

    int sum(int n)
    {
        int ret = 0;
        while(n)
        {
            int tmp = n % 10;
            ret += tmp * tmp;
            n/=10;
        }
        return ret;
    }
    bool isHappy(int n) 
    {
        //定义快慢指针
        int slow = n;
        int fast = sum(n);
        while(slow != fast)
        {
            slow = sum(slow);
            fast = sum(sum(fast));
        }
        return slow == 1;
    }
};

个人思考】:在这个问题中,我们需要根据需求特性判断是否形成闭环,而闭环的判断条件就是是否出现重复数。最初,这个思路并不容易想到,但可以借助鸽巢原理,通过数据的最大值来推导可能的变化范围。**因此,在求解范围时,可以考虑是否能利用数据的最大值来确定 n的界限。**闭环会限制变化的范围。


11.盛水最多容器[对撞指针、单调性]

题目展示】:盛水最多容器

在这里插入图片描述

  • 输入:[1,8,6,2,5,4,8,3,7]
  • 输出:49
  • 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题目解析】:

1.解法一:暴力求解(会超时)

枚举出能构成的所有容器,找出其中容积最⼤的值,直接两层for循环,枚举能构成容器的体积,求得最大值。

代码展示:

class Solution 
{
public:
	int maxArea(vector<int>& height) 
	{
		int n = height.size();
		int ret = 0;
		// 两层 for 枚举出所有可能出现的情况
	for (int i = 0; i < n; i++)
    {
		for (int j = i + 1; j < n; j++) 
			{
				// 计算容积,找出最⼤的那⼀个
				ret = max(ret, min(height[i], height[j]) * (j - i));
				}
		}
    return ret;
}
};

2.解法二:对撞指针

算法思路

首先,我们需要理解如何计算容器的体积。通过设置 leftright 两个指针,分别指向容器的左边和右边,然后根据短板效应来决定水的高度,即水的高度由两边中较短的那块木板决定。

公式:

int v =  min(higth[left], higth[right]) * (right - left);

这里 v 代表容器的体积,其中有两个变量控制体积:heightwidthheight 是水的高度,width 是容器的宽度。

假设左边木板比右边木板短(即短板在左边),我们可以从这里分析水的容积变化。(这步骤可以暂时省去很多考虑)

容积变化的分析

  1. 容器的宽度会变小
    无论我们如何调整左或右边界,容器的宽度始终会减小(wide ↓),这意味着容积的变化必然受到宽度减少的影响。
  2. 移动左边界(短木板)
    改变左边界(短木板),由于左边界较小,新的水面高度不确定,但是不会超过右边界的高度,因此容器的容积可能会增大,导致v(未知) = w↓ * h(未知,可以增大)
  3. 移动右边界(长木板)
    由于右边界较大,无论有边界移动到哪里,新的水面高度一定不会超过左边界,意味着当前高度h不变,由于宽度不断变小,对于容积一定会变小的。v↓ = w↓ * h(↓ 或者 不变)

在这里插入图片描述

当我们移动短木板,这里因为h的不确定性,导致了容积可大可小。对此,当我们记录完一个区间的体积,将短木板往长木板靠拢,不间断判断下一个边界情况,不断刷新最大的容积

代码展示】:

class Solution 
{
public:
    int maxArea(vector<int>& height) 
    {
        //需要取最小的数据
        int left = 0;
        int right = height.size() - 1;
        int ret = 0;
        while(left < right)
        {
            //算体积
            int v = min(height[left],height[right])*(right - left);
            
            //更新最大的体积
            ret = max(ret, v);
            
            if(height[left] <= height[right])  left++;
            else right--;
        }
        return ret;
    }
};

个人思考】:遇到这类涉及公式计算最值的问题时,可以利用单调性来简化分析。关键在于如何选择移动边界:移动长木板时,容积必然减小,而移动短木板时,容积变化不确定,但有可能增大。

本质上,问题的核心是利用单调性,从大到小向内枚举,逐步更新容积。每次移动边界时,更新容积并与当前最大值进行比较,最终得到最大的容积。


611.有效三角形的个数[对撞指针、单调性]

题目展示】:611.有效三角形的个数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 输入: nums = [2,2,3,4]
  • 输出: 3

解释:有效的组合是:

  • 2,3,4 (使用第一个 2)
  • 2,3,4 (使用第二个 2)
  • 2,2,3

算法思路

1.数学知识:如何通过三个数,判断是否能构成三角形

只需要两边之和大于第三边
    a + b > c
    a + c > b
    b + c > a

2.解法一:暴力解法

通过暴力枚举法,可以使用三层for循环遍历所有可能的三角形数据,记录并筛选出符合条件的组合。

	 for(i = 0; i < n; i++)
        for(j = i + 1; j < n; j++)
            for(k = j + 1; k < n; k++)
                check(i, j , k);

通过数学优化,当a <= b <= c时,判断三角形成立只需验证a + b > c。因为在这种情况下,c是最大的,a + cb + c必然大于另一个边。优化步骤:首先对数组进行排序,得到有序数组。

时间复杂度

没有进行优化,三层for循环的时间复杂度就是O(3N^3^)。如果进行了优化,时间复杂度就是O(NlogN + N^3^)。虽然时间复杂度是取主要影响的变量,但是不管如何,这里进行了优化的情况下,时间复杂度是得到了优化,同时处理数据方面也是得到改善。

2.解法二:对撞指针

提示:借鉴上次容积问题的思路,**当根据公式或表达式判断条件时,可以利用单调性优化。**通过固定最大数,并使用leftright指针指向左右两端,避免枚举。类似容积问题,从左到右或从右到左的差异源自数据大小顺序,影响判断条件的判断效率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过设置两个变量作为边界,首先判断a + b是否大于c。如果a + b > c,那么从左到右时,a + b会始终大于c,无需再继续枚举;从右到左时,a + b的大小关系不确定,因此需要保留这个操作进行整体判断。如果a + b <= c,则从右到左会使b变小,导致无法满足条件,因此需要移动left,使得a + b不断逼近并超过c。在此过程中,leftright会不断调整,因此需要在循环内进行相应的更新。

这里的 sum += right - left 表示以 right 为边界时,所有满足条件的组合数量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码展示】:

class Solution 
{
public:
    int triangleNumber(vector<int>& nums)
    {
      sort(nums.begin(), nums.end());
        int n = nums.size();
        int sum = 0;
        for(int i = n - 1; i >=2; 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;
    }
};

179.和为s的两个数字[对撞指针、单调性]

题目展示】:179.和为s的两个数字(原题目))

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 输入:price = [3, 9, 12, 15], target = 18
  • 输出:[3,15] 或者 [15,3]

算法思路

这道题属于基础题,主要考察双指针法在单调性匹配中的应用。关键是判断 left + right == target

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于 left + right ? target,共有三种情况。通过利用单调性,依据 leftright 指向的数据关系,调整它们的位置以达到目标。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码展示

class Solution 
{
public:
    vector<int> twoSum(vector<int>& price, int target) 
    {
        int left = 0, right = price.size() - 1;
        
        while(left < right)
        {
            int sum = price[left] + price[right];

            //连续判断还是写else if分支语句
            if(sum > target) right--;
            else if(sum < target) left++;
            else return {price[left], price[right]};
        }
        //为了照护编译器,通过返回-1
        return {-1, -1};
    }
};

15.三数之和[对撞指针、单调性]

题目展示】:15.三数之和

在这里插入图片描述

输入: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] 。
注意,输出的顺序和三元组的顺序并不重要。

首先分析题目给出的信息,注意到题目没有明确说明是否允许重复三元组。因此,需要通过实例来推断是否存在重复三元组的情况。
在这里插入图片描述

题目中说明三元组的顺序不重要,因此我们关注的是数据是否重复。通过例子 [-1, 0, 1][0, 1, -1][-1, 1, 0],我们可以发现这些是重复的三元组。为了简化判断,可以统一将三元组排序为 [-1, 0, 1],通过排序来优化,避免不必要的重复判断

3.解法一:排序 + 暴力枚举 + 利用set去重:时间复杂度O(N3)

在这里插入图片描述

4.解法二:对撞指针

根据题目需求,我们需要统计满足 nums[a] + nums[left] + nums[right] == 0 的三元组。可以将其转化为 nums[left] + nums[right] = -nums[a],这意味着当 nums[left] + nums[right] 等于 nums[a] 的相反数时,条件成立。通过固定一个数值并移动两个边界,我们能够减少不必要的枚举次数。

个人思考】:这个问题和两数之和的单调性问题类似,只需固定一个数并让另两个数的和等于目标值,之后通过调整左右指针来查找所有满足条件的组合。

细节问题

在这里插入图片描述

如果使用 set 来去重,则需要额外的时间来插入和查找每个元素,时间复杂度为 O(log n)。我们通过排序的方法,将[-1, 0, 1]、[0, 1, -1]、[-1, 1, 0]重复的数据统一变成了[-1, 0, 1]的形式,但是重复的数据,我们是不需要的。固定一个数,当leftright指向位置符合要求后,就需要考虑重复问题,进行去重操作。当然不止leftright需要去重,固定的数据也需要完成去重操作,避免越界[0, 0, 0, 0]

在这里插入图片描述

代码展示

class Solution
{
    public:
    vector<vector<int>> threeSum(vector<int>& nums) 
    {
        vector<vector<int>> v;

        // -3 -2 -1
        //排序下
        sort(nums.begin(), nums.end());
        int n = nums.size();
        for(int i = 0; i < n - 2 ;  )
        {
            
            //不存在 nums[]+nums[] = minPositive_nums[i]
            if(nums[i] > 0) break;

            int left = i + 1, right =  n - 1;
            int target = -nums[i];

            while(left < right)
            {
                int sum = nums[left] + nums[right];
                if(sum > target) right--;
                else if(sum < target) left++;
                else
                {
                    //初始化列表自动转为vector<int>类型
                    v.push_back({nums[left], nums[right], nums[i]});
                    left++;right--;
                    //去重判断
                    while(left < right && nums[left] == nums[left - 1]) left++;
                    while(left < right && nums[right] == nums[right + 1]) right--;
                }
            }

            //去重操作
            //这里会到导致判断时,造成越界访问
            //while(nums[i] == nums[i + 1]) i++;
            i++;
            ///关于越界访问,需要判断循环逻辑是否有问题。
            while(i < n && nums[i] == nums[i - 1]) i++;
        }
        return v;
    }

18.四数之和(三数之和Plus)[对撞指针、单调性]

题目展示】:18.四数之和

在这里插入图片描述

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

算法思路

这里同样的,按照题目要求可以得到一个表达式nums[a] + nums[b] + nums[left] + nums[right] == target,按照我们熟悉的解法,我们是通过固定一个数,以left和right两个数作为边界向内进行查找。但是这里多出了一个数,那么不妨可以这样 nums[b] + nums[left] + nums[right] == target - nums[a],跟三数之和题目不是一样了吗?这里多次一个数的意义,就是多了一层循环。

1.解法一:排序 + 暴力枚举 + 利用set去重 时间复杂度O(N4)

2.解法二:对撞指针

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

问题:栈溢出

在这里插入图片描述

对此这里需要考虑数据的范围将desttarget类型转化为long long

代码展示

class Solution 
{
    public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) 
    {
        //-4 - 3 -2 -1
        sort(nums.begin(), nums.end());

        int n = nums.size();
        vector<vector<int>> v;
        for(int i = 0; i < n - 3;)
        {
            for(int j = i + 1; j < n - 2;)
            {
                //新的目标数
                long long dest = (long long)target - nums[i] - nums[j];
                int left = j + 1, right = n - 1;
                while(left < right)
                {
                    int sum = nums[left] + nums[right];
                    if(sum > dest) right--;
                    else if(sum < dest) left++;
                    else
                    {
                        v.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 < n - 2 && nums[j] == nums[j - 1])  j++;
            }
            i++;
            while(i < n - 3 && nums[i] == nums[i - 1])  i++;
        }
        return v;
    }
};

在这里插入图片描述
快和小二一起踏上精彩的算法之旅!关注我,我们将一起破解算法奥秘,探索更多实用且有趣的知识,开启属于你的编程冒险!

评论 61
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

是店小二呀

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值