首先,介绍一下双指针算法。
我们在用朴素算法暴力解决问题时,通过挖掘某些性质,使得算法复杂度由 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;
}