【算法】双指针

【ps】本篇有 8 道 LeetCode OJ

目录

一、算法简介

二、相关例题

1)移动零

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

2)复写零

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

3)快乐数

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

4)盛最多水的容器

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

5)有效三角形的个数

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

6)查找总价格为目标值的两个商品

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

7)三数之和

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

8)四数之和

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)


一、算法简介

双指针(或多指针)是一种针对于线性结构的方法,适用于任何包含顺序存储的线性数据结构中,通过分别控制位于序列不同位置的两个指针,在序列中的移动,逐步简化待解决问题,以求解问题或满足特定条件。

二、相关例题

1)移动零

283. 移动零 - 力扣(LeetCode)

.1- 题目解析

这是一道典型的数组划分问题,可以选用双指针方法求解。

题目要求,将一个数组中的所有 0 移动到数组末尾,如此,数组就被划分为了非 0 和 0 两部分。也就是说,我们的目标是将非 0 元素集中放在数组左边,将 0 集中放在数组右边。

现定义两个指针 cur 和 dest,其含义和功能如下:

  • cur:负责从左往右扫描数组,即遍历数组。
  • dest:负责处理数组元素,dest 指向已处理区间内,非 0 元素的最后一个位置。

在实际处理元素的过程中,dest 处理元素的速度一定要慢于 cur 遍历数组的速度,也就是说,dest 基本在 cur 之前。如此,cur 和 dest 又将数组分为了三个区间:

  • [ 0, dest ]:非 0 区间(已处理区间)。
  • [ dest + 1, cur - 1 ]:0 区间(已处理区间)。
  • [ cur, 数组长度 - 1 ]:待处理区间。

在数组中保持这三个区间,直到 cur 遍历至数组末尾,那么整个数组就是已处理的区间,被分为了非 0 和 0 两部分。

至于元素的处理方式,不难想到是交换非 0 元素和 0 元素。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        //初始时,初始化dest为-1,使其在cur左侧,保持三个区间的划分
        int dest=-1,cur=0;
        
        while(cur<nums.size()) 
        {
            if(nums[cur]==0)
            {
                cur++; //cur负责遍历数组,找到数组中的非0元素
            }
            else
            {
                swap(nums[dest+1],nums[cur]); //dest负责处理0元素,方式是与cur交换非0元素
                cur++;
                dest++;
            }
        }
    }
};

将以上代码优化为:

cpp 复制代码
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]);
            }
        }
    }
};

2)复写零

1089. 复写零 - 力扣(LeetCode)

.1- 题目解析

这道题最简单的解决方式,就是创建一个规模相同的新数组,按题目要求,通过双指针将原数组的元素按顺序抄入新数组中,更具体为,一个指针指向原数组待拷贝的元素,另一个指针指向新数组要拷贝的位置。但这是异地操作,题目要求的是就地操作,那么,我们其实可以在原数组上,用双指针模拟异地操作。

现定义两个指针 cur 和 dest,其含义和功能如下:

  • cur:负责遍历数组。
  • dest:负责处理数组元素,进行 0 的复写。

不难发现,如果 cur 是从左往右遍历数组的,那 dest 在进行 0 的复写时,会覆盖非 0 元素,即将其从数组中删除了,这显然会得到错误答案。因此,cur 不能从左往右遍历数组。

观察题目示例,在进行复写 0 后,数组的规模并未发生变化,这就意味着会有一些元素在进行复写 0 后丢失。由此,可以先让 cur 找到最后一个不会被丢弃的非 0 元素,然后从右往左遍历数组;而 dest 在 cur 的右侧,也从右往左遍历数组,进行复写 0。

要让 cur 找到最后一个不会被丢弃的非 0 元素,首先要从左往右遍历数组,在原数组上模拟一遍异地操作的过程:

  1. 判断 cur 位置的值,若非 0 则 dest 向后移动一步,若为 0 则 dest 向后移动两步;
  2. cur 始终向后移动一步,直到 dest 先到达数组末尾,此时 cur 就找了最后一个不会被丢弃的非 0 元素。

但是这样存在越界的可能,若 dest 距离数组末尾还有一步,但 cur 遇到了 0 ,dest 向后移动两步就越界了,因此需要处理这种边界问题。具体的方式是,当 dest 越界时,将数组末尾的元素改为 0 ,让 cur 向左移动一步,让 dest 向左移动两步。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        int cur=0,dest=-1,n=arr.size();
        // 1. 先找到最后⼀个非0的数 
        while(cur<n)
        {
            if(arr[cur]) 
                dest++;
            else
                dest+=2;
            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--];
            else
            {
                arr[dest--]=0;
                arr[dest--]=0;
                cur--;
            }
        }
    }
};

3)快乐数

202. 快乐数 - 力扣(LeetCode)

.1- 题目解析

示例 1 的推导过程如下:

示例 2 的推导过程如下:

由此可见,每个数的推导过程都会形成一个环,满足题目条件的数最终会在"1"上成环,而不满足题目条件的数虽然也会成环,但环里不会有"1"。

那么,本题的求解过程就从判断一个数是否是快乐数,转化成了一个环中是否有"1",类似于判断一个链表是否有环。如此,就可以使用快慢双指针。

现定义两个指针 slow 和 fast,其含义和功能如下:

  • slow:每次向后移动一步。
  • fast:每次向后移动两步。

当 slow 和 fast 在一个环上有先有后,但最终一定会相遇,相遇时,判断当前的值是否为 1 即可。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    int bitSum(int n) //按题目要求推导出下一个数
    {
        int sum=0;
        while(n)
        {
            int t=n%10;
            sum+=t*t;
            n/=10;
        }
        return sum;
    }
    bool isHappy(int n) {
        int slow=n,fast=bitSum(n);     //快指针指向慢指针的下一个数
        while(slow!=fast)
        {
            slow=bitSum(slow);         //慢指针每次移动一步
            fast=bitSum(bitSum(fast)); //快指针每次移动两步
        }
        return slow==1;                //最终判断相遇时的值是否为1即可
    }
};

4)盛最多水的容器

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

.1- 题目解析

由题,要找到盛最多水的容器,实际是要找到数组中的两个位置,使其所构成的矩形的高宽之积最大。

首先不难想到暴力枚举,用两次循环将所有数的组合枚举出来,很容易就找到最大的高宽之积,但这样显然会超时。

不过,寻找最大的高宽之积,这个思路肯定是对的。要高宽之积最大,也就是要高尽量大、宽也尽量大。

如图,在示例 1 中截取一小段数来尝试找最大的高宽之积,不难发现,在向内枚举寻找高宽之积的过程中,高宽之积明显是不断在变小的,因为向内枚举时宽始终在变小,而高不论是在变小还是不变,这使得高宽之积都会变小。

由此,就可以利用这种单调性,将暴力枚举的方式优化。具体方式是,让双指针指向数组左右两边,让它们向中间靠近,逐个枚举出高宽之积,每次枚举完,其中一个值更小的指针就向中间移动一步;两个指针相遇后,就在已经枚举出的高宽之积中,找出一个最大值即可。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    int maxArea(vector<int>& height) {
        int left=0,right=height.size()-1;
        int sum=0;
        while(left<right)
        {
            int v=min(height[left],height[right])*(right-left); //计算出当前双指针所找出的高宽之积
            sum=max(sum,v);                //统计最大的高宽之积
            if(height[left]<height[right]) //移动指针
                left++;
            else
                right--; 
        }
        return sum;

    }
};

5)有效三角形的个数

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

.1- 题目解析

题目要求,从数组中随机选出三个数,以此枚举出可以组成三角形的所有组合。不难想到暴力枚举 + 判断的方式,但要枚举三个数就要用到三层循环,这就一定会超时,因此需要对此种方法进行优化。

三个数能够构成一个三角形,一定满足"任意两边之和大于第三边"这个条件。假设三角形的三边分别为 a、b、c,满足"任意两边之和大于第三边",即满足"a + b > c && b + c > a && a + c > b"。不过,我们可以对此做一个小优化,如果"任意两边之和大于第三边"的前提是,a <= b <= c,那么 a、b、c 仅需满足"a + b > c"即可构成一个三角形,因为此时 c 是最大数,不论加上 a 还是 b 都会大于另一个数。

由此,在对三个数进行枚举之前,只需对数组进行升序排序,即可完成优化,然后利用数组的单调性和双指针来进行解题。具体的方式是,将数组末尾的数,即当前升序数组中最大的数设为 c,将数组头部和倒数第二的数分别设为 a 和 b,去与 c 进行比较,以找出满足条件的三数组合;每次比较完,都让 a 的指针向数组右侧移动一位,直到 a 和 b 相遇,就将数组的倒数第二个数设为 c,进行下一轮比较;以此类推,直到将 c 数组的第三个数设为 c,判断完此轮的"a + b > c"即完成了全部题解过程。

而在判断"a + b > c"的过程中,也存在优化的可能。当" a + b > c "不满足时,由于数组是升序的,a 当前的值就是这一轮中最小的,a 的指针再向数组右侧移动一位,也可以使" a + b > c "成立,因此,此时只需统计 a 到 b 之间数的个数,然后直接进入下一轮判断即可。

.2- 代码编写

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

6)查找总价格为目标值的两个商品

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

.1- 题目解析

尽管这道题用暴力枚举也会超时,但可以和上文的题目一样,通过"双指针 + 单调性"来求解。

题目中的数组已经是升序的了,只需用双指针分别指向数组的头部和尾部。数组的头部是数组中最小的数,尾部是数组中最大的数,如果双指针的值之和小于 target,则让指向数组头部的指针向右移动一步,使和增大,反之,则让让指向数组尾部的指针向左移动一步,使和减小,直至找出满足条件的数组元素。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& price, int target) {
	int left=0,right=price.size()-1;
		while(left<right)
		{
			if(price[left]+price[right]>target)
			{
				right--;
			}
			else if(price[left]+price[right]<target)
			{
				left++;
			}
			else 
			{
				return {price[left],price[right]};
			}
		 } 
		return {-1,-1};
    }
};

7)三数之和

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

.1- 题目解析

题目要求从一个数组中,找出不重复的、和为 0 的无序三元组。

我们可以通过暴力枚举,找出所有符合条件的三元组,然后进行去重。但直接去重并不方便,如果先对数组进行升序排序,再去找出所有符合条件的三元组,再进行去重,就要方便得多。不过,这显然要用到三层循环,也是要超时的。

三个数的和为 0 ,其实可以将其中一个数作为基准,去找另外两个数,使它们的和为这个数的相反数。

如图,将数组头部的元素 -4 作为基准,只需在剩下元素构成的区间中,寻找两个和为 4 的数即可。

由此,就得到了该题的解题步骤:

  1. 对数组进行升序排序;
  2. 将一个数作为基准值;
  3. 在剩下的区间里,找到两数之和为基准值的相反数。

其中,还涉及一些细节,例如要在剩下的区间里找完所有满足条件的两个数,不能遗漏;此外,还需要对找到的结果进行去重。

去重的具体方式是,在区间中找到一个结果后,如果 left 或 right 在移动时遇到了重复的元素,就要将其跳过,因为这个重复元素已经被统计过了;找完了当前的区间,如果 i 在移动时遇到了重复的元素,也要将其跳过;i、left、right不能越界。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ret;
        //1.排序
        sort(nums.begin(),nums.end());
        int i=0,n=nums.size();
        while(i<n)
        {
            if(nums[i]>0) //小优化
            {
                break;
            }
            //2.选基准值
            int left=i+1,right=n-1,target=-nums[i];
            //3.在剩余区间中查找两个数
            while(left<right)
            {
                int sum=nums[left]+nums[right];
                if(sum>target)
                {
                    right--;
                }
                else if(sum<target)
                {
                    left++;
                }
                else
                {
                    ret.push_back({nums[i],nums[left],nums[right]});
                    left++;right--;
                    //去重 left
                    while(left<right&&nums[left]==nums[left-1])
                    {
                        left++;
                    }
                    //去重 right
                    while(left<right&&nums[right]==nums[right+1])
                    {
                        right--;
                    }
                }
            }
            i++;
            //去重 i
            while(i<n&&nums[i]==nums[i-1])
            {
                i++;
            }
        }
        return ret;
    }
};

8)四数之和

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

.1- 题目解析

这道题与上一道《三数之和》类似,也可以通过排序 + 双指针来进行求解,具体的方式是,

先确定一个基准值 a ,然后在剩余的区间里找到和为"目标值 - a"的三个数即可;而对于和为目标值 - a 的三个数,可以套用先前的思路,在该区间里确定一个基准值 b,然后在剩余的区间里找到和为"目标值 - a - b"的两个数即可。余下的细节问题,也与上一道《三数之和》类似,也要注意去重和不漏。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
		vector<vector<int>> ret;
        //1.排序
		sort(nums.begin(),nums.end());
        //2.选基准值 a
		int a=0,n=nums.size();
		while(a<n)
		{
            //3.选基准值 b
			int b=a+1;
			while(b<n)
			{
                //4.在剩余区间里找和为target-a-b的两个数
				int left=b+1,right=n-1;
				long long aim=(long long)target-nums[a]-nums[b];//防溢出
				while(left<right)
				{
					int sum=nums[left]+nums[right];
					if(sum<aim)
					{
						left++;
					}
					else if(sum>aim)
					{
						right--;
					}
					else
					{
						ret.push_back({nums[a],nums[b],nums[left++],nums[right--]});
                        //去重left
						while(left<right&&nums[left]==nums[left-1])
						{
							left++;
						}
                        //去重right
						while(left<right&&nums[right]==nums[right+1])
						{
							right--;
						}
					}

				}
				b++;
                //去重b
				while(b<n&&nums[b]==nums[b-1])
				{
					b++;
				}
			}
			a++;
            //去重a
			while(a<n&&nums[a]==nums[a-1])
			{
				a++;
			}
		}
		return ret;
    }
};
相关推荐
小林熬夜学编程11 分钟前
C++第五十一弹---IO流实战:高效文件读写与格式化输出
c语言·开发语言·c++·算法
蠢蠢的打码16 分钟前
8584 循环队列的基本操作
数据结构·c++·算法·链表·图论
程序猿进阶3 小时前
如何在 Visual Studio Code 中反编译具有正确行号的 Java 类?
java·ide·vscode·算法·面试·职场和发展·架构
Eloudy3 小时前
一个编写最快,运行很慢的 cuda gemm kernel, 占位 kernel
算法
king_machine design3 小时前
matlab中如何进行强制类型转换
数据结构·算法·matlab
西北大程序猿3 小时前
C++ (进阶) ─── 多态
算法
无名之逆3 小时前
云原生(Cloud Native)
开发语言·c++·算法·云原生·面试·职场和发展·大学期末
头发尚存的猿小二3 小时前
树——数据结构
数据结构·算法
好蛊3 小时前
第 2 课 春晓——cout 语句
c++·算法
山顶夕景3 小时前
【Leetcode152】分割回文串(回溯 | 递归)
算法·深度优先·回溯