刷算法的时候遇到过不少双指针有关的问题,但是从来都没有总结过这一问题。今天下午碰到了正好总结一下,嘿嘿~。
首先,双指针与其说是一种算法,倒不如说是一种优化的策略。具体来说,是基于“单调性”对暴力枚举的一种的优化策略。一个双指针问题通常都有一个需要消耗O()暴力的做法:
for(int i = 0; i < n; i++){
for(int j = 0; j < i; j++){
if(//检查a[i]与a[j]是否满足某个性质)
}
}
一般双指针做法就是基于题目中给出的单调性,使得我们每次枚举 j 这个循环变量的时候不需要从0开始枚举,而是从之前j所在的位置开式往后枚举。这样就可以将O()降到O(n),模版也从上面那个变成了下面这个:
for(int i = 0, j = 0; i < n; i++){
while(j < i && check(i, j)) j++;//check函数用于检查a[i]和a[j]是否满足某一个性质
}
那么废话不多说,一起来看看一些有关双指针的具体问题吧!
1.最长连续不重复子序列
给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
输入格式
第一行包含整数 n。
第二行包含 n个整数(均在 0∼100000范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。
数据范围
1≤n≤100000
输入样例:
5 1 2 2 3 5
输出样例:
3
题解:
我们先假定一个我们已经得到了一个区间 [ j , i ], 这个区间表示的是一个连续不重复的子序列。在这个区间中,i 这个位置是固定的(我们把它当做之前模版中第一重循环的循环变量, 它是依次递增的),并且 [ j , i ] 区间中取的这个 j 的值能够保证 [ j , i ] 是所有以 i 结尾的序列中最长的一个(换言之 [ j - 1 , i] 就不是一个连续不重复的子序列了)。通过这样处理,当我们枚举下一个i变量的时候, j是不需要从下标0开始重新枚举的,而是只要从原先所在的位置开始枚举就行了, 那么这个时候我们只要将j从原来的位置向右移直到形成了一个新的连续不重复的子区间 [ j , i ]即可。
思路就是这样,以下是C++的具体实现代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N]; //数组a是原数组
int s[N]; //数组s存储了k属于[j, i]区间中a[k]出现的次数
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
int res = 0;
for(int i = 0, j = 0; i < n; i++){
s[a[i]] ++ ; //a[i]是新加入区间[j, i]的数字,因此我们给它计数加一
while(s[a[i]] > 1){ //当这个新加入的数字出现了重复的问题我们将j指针右移直至重复问题解决
s[a[j]] -- ;
j ++ ;
}
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
2.判断子序列
给定一个长度为 n 的整数序列 a1,a2,…,an 以及一个长度为 m 的整数序列 b1,b2,…,bm。
请你判断 a 序列是否为 b 序列的子序列。
子序列指序列的一部分项按原有次序排列而得的序列,例如序列 {a1,a3,a5} 是序列 {a1,a2,a3,a4,a5} 的一个子序列。
输入格式
第一行包含两个整数 n,m。
第二行包含 n 个整数,表示 a1,a2,…,an。
第三行包含 m 个整数,表示 b1,b2,…,bm。
输出格式
如果 a 序列是 b 序列的子序列,输出一行
Yes
。否则,输出
No
。数据范围
1≤n≤m≤100000
≤ai,bi≤输入样例:
3 5 1 3 5 1 2 3 4 5
输出样例:
Yes
题解:
这个问题是要我们判断a是否是b的子序列。同样是用双指针pa 和 pb,这两个指针分别指向了a[pa] 和 b[pb]。我们依次递增pb指针,一旦发现a[pa] == b[pb], 就说明a[pa]这个值出现在了b的子序列中,我们就将pa指针向后移。最终循环完了pb,我们再判断pa是否走到了a的末尾(如果走到了末尾则说明a中的所有元素都保序的出现在了b中,也就是a是b的一个子序列)
C++代码如下:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int a[N], b[N];
bool isSubSeq(int a[], int n, int b[], int m){
int pa = 0;
for(int pb = 0; pb < m; pb++){
if(pa < n && a[pa] == b[pb]) pa ++ ;
}
return pa == n;
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
for(int i = 0; i < m; i++) scanf("%d", &b[i]);
if(n > m){
printf("No\n");
}else{
bool res = isSubSeq(a, n, b, m);
if(res) printf("Yes\n");
else printf("No\n");
}
return 0;
}
3.数组元素的目标和
给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。
数组下标从 00 开始。
请你求出满足 A[i]+B[j]=x 的数对 (i,j)。
数据保证有唯一解。
输入格式
第一行包含三个整数 n,m,x,分别表示 A 的长度,B 的长度以及目标值 x。
第二行包含 n 个整数,表示数组 A。
第三行包含 m 个整数,表示数组 B。
输出格式
共一行,包含两个整数 i 和 j。
数据范围
数组长度不超过 。
同一数组内元素各不相同。
1≤数组元素≤输入样例:
4 5 6 1 2 4 7 3 4 6 8 9
输出样例:
1 1
题解:
这个问题非常的精妙,一开始我也没想到,只是想到了一个O(nlogn)的做法(就是循环a数组的每个数 a[i] ,然后用二分查找在b数组中查询是否有值为x - a[i]的元素)。O(n)的具体解法是这样的:我们先假定我们在最外层循环有一个从1开始递增的变量i,i是指向变量a[i]的指针。现在将i固定,我们保证a[i] + b[j] >= x的并且j是使得该不等式成立的最小的一个j。那么我们会得到两种情况 (1) a[i] + b[j] == x,那么这个就是答案了,那后面就不用处理了 (2) a[i] + b[j] > x,此时由于我们已经知道了当前的j是这个不等式成立的最小的一个j,那么我们就不需要枚举j之后的数了,因为a[i] + b[j + 1] > x, a[i] + b[j + 2] > x ...
C++代码如下:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1e5 + 10;
int n, m, x;
int A[N], B[N];
int main(){
scanf("%d%d%d", &n, &m, &x);
for(int i = 0; i < n; i++) scanf("%d", &A[i]);
for(int i = 0; i < m; i++) scanf("%d", &B[i]);
int res1, res2;
for(int i = 0, j = m - 1; i < n; i++){
//找到A[i] + B[j] >= x 并且保证j是最小的
while(j >= 1 && A[i] + B[j - 1] >= x) j -- ;
if(A[i] + B[j] == x){
res1 = i, res2 = j;
break;
}
}
cout << res1 << ' ' << res2 << endl;
return 0;
}
4.有效三角形的个数(leetcode 611题)
给定一个包含非负整数的数组
nums
,返回其中可以组成三角形三条边的三元组个数。示例 1:
输入: nums = [2,2,3,4] 输出: 3 解释:有效的组合是: 2,3,4 (使用第一个 2) 2,3,4 (使用第二个 2) 2,2,3示例 2:
输入: nums = [4,2,3,4] 输出: 4提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 1000
题解:我们不难发现最暴力的做法是O() 的时间复杂度,肯定过不了。那么我们可以考虑用双指针将原问题降为O()的时间复杂度。双指针其实最重要是要发现一些跟“单调性”有关的性质。在这个问题中我们先对原数组进行一个从大到小的排序,第一维循环枚举k,k表示的是三角形中的最长的那条边,剩下的两个指针i,j分别用来枚举三角形的较短的那两条边,因此我们只要保证nums[i] + nums[j] > nums[k]就可以构成一个三角形,这个时候我们可以想办法让i,j的枚举控制在O(n)的时间内,就可以将整体的时间复杂度降到O()了。我们使用的策略如下:初始的时候i == 0, j == k - 1。当i < j的时候,我们先移动i指针使得 nums[i] + nums[j] > nums[k]的,这个时候 nums[i ~ j - 1]和nums[j]和nums[k]三边一定能组成三角形(因为排完序的序列是递增的),因此答案加上j - i,然后我们再将j 减1,再重复上面移动i的过程。我们可以发现i和j的移动都是从上次停下的位置继续开始的,因此时间复杂度是O(n)
代码如下:
class Solution {
public:
int triangleNumber(vector<int>& nums) {
if(nums.size() < 3){
return 0;
}
sort(nums.begin(), nums.end());
int res = 0;
for(int k = 2; k < nums.size(); k++){
int i = 0, j = k - 1;
while(i < j){
while(i < j && nums[i] + nums[j] <= nums[k]) i++;
if(i == j) break;
res += j - i;
j -- ;
}
}
return res;
}
};
.
感谢大家的阅读。