两大类 “双指针” 算法剖析【附例题详解+AC代码】

首先,介绍一下双指针算法。
我们在用朴素算法暴力解决问题时,通过挖掘某些性质,使得算法复杂度由 O(n^2)->O(n) ,我们把具有这样性质的算法称为双指针算法【其实双指针算法非常广泛,不只是被用在维护两个窗口上,但在这里,我们缩小了它的范围】。

常用的两种双指针算法的类型:
一种是: 两个指针分别指向两个序列 ( “归并排序” 就用到了这一种指针,具体操作:每一次分别移动两个指针,两个指针移动完的时候,排序过程即结束);
另一种是: 两个指针指向同一个序列,一个指向开头,一个指向结尾 (如快排)。
在这里插入图片描述
在这里插入图片描述
我们令红色指针表示 i,绿色指针表示 j 。

以上两种类型的指针所举的例子可详见 两大经典实用排序(快排和归并)

常用写法(双指针算法的通用模板)

for(int i=0;i<n;i++)
{
    
    
	while(j<i && check(i,j))  j++;
	
	//每道题的具体逻辑
	 
}

双指针算法最核心的用途: 运用某一单调的性质进行优化【将朴素算法,也就是暴将力解法优化到 O(n)】,因为每个指针用到的次数不超过 n 。

//一般的朴素做法(暴力解法)
for(int i=0;i<n;i++)
    for(int j=0;j<n;j++)
        O(n^2) //时间复杂度

下面我们用一个简单的例子来说明 i 和 j 这两个指针是如何运作的

//问题:输入一个字符串,将其中的单词逐个输出

#include <bits/stdc++.h>
using namespace std;

int main()
{
    
    
	char str[1000];
	gets(str);
	int n=strlen(str);
	
	for(int i=0;i<n;i++)
	{
    
    
		int j=i;
		while(j<n &&str[j]!=' ') j++;
	
		/* 这道题的具体逻辑:第一次 i指向第一个单词的首字母,
		  j结束时指向第一个空格,输出第一个单词并换行,令 i=j,i指向第一个空格 
		 下一次最外层的循环中,i++,i指向第二个单词的首字母,同上操作 */
		for(int k=i;k<j;k++) cout<<str[k];  
		cout<<endl;
		
		i=j;
	}
	return 0;
} 

运行结果:
在这里插入图片描述
例题1【第二种双指针算法】:最长连续不重复子序列(若不要求连续,可去重来做)

题目:
给定一个长度为n的整数序列,请找出最长的不包含重复数字的连续区间,输出它的长度。

输入格式
第一行包含整数n。

第二行包含n个整数(均在0~100000范围内),表示整数序列。

输出格式
共一行,包含一个整数,表示最长的不包含重复数字的连续子序列的长度。

数据范围
1≤n≤1000001≤n≤100000
输入样例:
5
1 2 2 3 5
输出样例:
3
在这里插入图片描述
红色指针表示 i,绿色指针表示 j

首先,我们想一下朴素做法怎么解决,再去考虑双指针算法来优化
朴素做法: 假设 i 是终点,j 是起点,先枚举终点 i,再枚举起点 j【起点和终点可相同】。

// 朴素做法: O(n^2)
for(int i=0;i<n;i++)
    for(int j=0; j<=i; j++)
        if(check(i,j))
        {
    
    
        	res = max(res,i-j+1);
		}
// 双指针算法  O(n)
for(int i=0,j=0; i<n; i++)
{
    
    
	while (j<=i && check(i,j)) j++;
	res = max(res,i-j+1);
}

本质: j 尽可能与 i 的距离变大,并保证没有重复数字

原理: 当 i 向右移动时,j 指针不会回退,只能不动或者向右移动 (如果 j 需要向右移动,则表示表示出现重复数字了,此时只需要把 j 向右移动,直到 i 和 j 之前没有重复数字为止)。即:保持一定的单调性。

// 附 AC代码
/*在数据范围不是很大时,check【j】可以这样考虑,开一个动态的数组记录每个数出现的次数,
加上第i个数重复时,就让它出去,从而化简 check【j】,使得a[j]!=a[i] */

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;

int n;
int a[N],s[N];

int main()
{
    
    
	cin>>n;
	for(int i=0;i<n;i++) cin>>a[i];
	
	int res = 0;
	for(int i=0,j=0; i<n; i++)
	{
    
    
		s[a[i]]++; //记录出现的次数 
		while (s[a[i]] > 1)
		{
    
     //出现相同数的时候,进行循环,i不变,j往后移,直到i和j指向同一个数,保证没有相同的数 
			s[a[j]]--; 
		    j++;
		}
		res = max(res,i-j+1); //每次更新最长连续不重复子序列的长度 
	}
	
	cout << res << endl;
	return 0;
} 

例题2【第一类双指针算法】:数组元素的目标和

题目:
给定两个升序排序的有序数组A和B,以及一个目标值x。数组下标从0开始。
请你求出满足A[i] + B[j] = x的数对(i, j)。
数据保证有唯一解。

输入格式
第一行包含三个整数n,m,x,分别表示A的长度,B的长度以及目标值x。

第二行包含n个整数,表示数组A。

第三行包含m个整数,表示数组B。

输出格式
共一行,包含两个整数 i 和 j。

数据范围
数组长度不超过100000。
同一数组内元素各不相同。
1≤数组元素≤pow(10,9)。
输入样例:
4 5 6
1 2 4 7
3 4 6 8 9
输出样例:
1 1
在这里插入图片描述

// 已知两个升序排序的有序数组 A和 B

// 首先想一下暴力的做法 时间复杂度:O(n*m),显然超时 
for(int i=0;i<=n;i++)
  for(int j=0;j<=m;j++)
    if(a[i]+b[j]==x) 
	  {
    
    cout<<i<<" "<<j;  break;}
	  
/*双指针算法的优化 (实质:满足单调性质【两个数组元素的值分别随 
i增大向右递增,随 j减小向左递减(已将两个数组升序排列)】) */
for(int i=0,j=m-1; i<n; i++)
{
    
    
	while(j>=0 && a[i]+b[j]>x) j--;
	if(a[i]+b[j] == x)	{
    
    cout<<i<<" "<<j;  break;}
}
/* 对于每一个 i,尽可能找一个最小的 j,满足 a[i]+b[j]>x(这个下标 j从大到小
递减来找,满足 a[i]+b[j]>x时则继续查找,当 j不能再减小时,增大 i,重复上述过程,
直到满足 a[i]+b[j]==x,即成功找到了一个值。 时间复杂度:O(n+m) */
// AC代码
#include <bits/stdc++.h>
#define read(x) scanf("%d",&x)
using namespace std;
const int N = 1e5+10;

int n, m, k;
int a[N], b[N];

int main()
{
    
    
    read(n), read(m), read(k);
    for(int i=0; i<n; i ++) read(a[i]);
    for(int i=0; i<m; i ++) read(b[i]);

    for(int i=0, j=m-1; i<n; i++) 
    {
    
    
        while(j>=0 && a[i]+b[j]>k) j--;
        if(j>=0 && a[i]+b[j]==k) 
        {
    
    
        	printf("%d %d\n",i,j);
        	break;
		}
    }
    
    return 0;
}

猜你喜欢

转载自blog.csdn.net/Luoxiaobaia/article/details/106182893