双指针算法

刷算法的时候遇到过不少双指针有关的问题,但是从来都没有总结过这一问题。今天下午碰到了正好总结一下,嘿嘿~。

首先,双指针与其说是一种算法,倒不如说是一种优化的策略。具体来说,是基于“单调性”对暴力枚举的一种的优化策略。一个双指针问题通常都有一个需要消耗O(^{n2})暴力的做法:

for(int i = 0; i < n; i++){
    for(int j = 0; j < i; j++){
        if(//检查a[i]与a[j]是否满足某个性质)
    }
}

一般双指针做法就是基于题目中给出的单调性,使得我们每次枚举 j 这个循环变量的时候不需要从0开始枚举,而是从之前j所在的位置开式往后枚举。这样就可以将O(^{n2})降到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
-10^{9}≤ai,bi≤10^{9}

输入样例:
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。

数据范围

数组长度不超过 10^{5}
同一数组内元素各不相同。
1≤数组元素≤10^{9}

输入样例:
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(n^{3 }) 的时间复杂度,肯定过不了。那么我们可以考虑用双指针将原问题降为O(n^{2})的时间复杂度。双指针其实最重要是要发现一些跟“单调性”有关的性质。在这个问题中我们先对原数组进行一个从大到小的排序,第一维循环枚举k,k表示的是三角形中的最长的那条边,剩下的两个指针i,j分别用来枚举三角形的较短的那两条边,因此我们只要保证nums[i] + nums[j] > nums[k]就可以构成一个三角形,这个时候我们可以想办法让i,j的枚举控制在O(n)的时间内,就可以将整体的时间复杂度降到O(n^{2})了。我们使用的策略如下:初始的时候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;
    }
};

.

感谢大家的阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值