双指针算法及模板

gao79138 / 2023-09-01 / 原文

双指针算法及模板

1.第一类双指针算法

img

    第一类双指针算法指的就是:
        有两个序列A和B,同时有两个指针i和j。使得i指向其中一个序列,j指向另外一个序列。典型的应用就是在归并排序当中,将两个序列按照某一个规则进行排序时(从小到大或从大到小),使用的就是双指针算法。

2. 第二类双指针算法

img

    第二类双指针算法指的就是:
        有一个序列A,同时有两个指针i和j。使得i和j同时指向一个序列。典型的应用就是在快速排序中,当调整区间时,就是采用双指针算法来进行的。

3. 双指针算法的应用

    双指针算法的应用是非常之广泛的。具体采用哪一类双指针算法要根据题目具体问题具体分析。

4. 双指针算法的作用

    双指针算法的核心作用就在于可以优化暴力算法。如果某种问题通过暴力做法达到了O(n)以上的时间复杂度的话,且这道题可以采用双指针算法求解的话,那么采用双指针算法可以将问题优化到O(n)的时间复杂度。一般而言,双指针算法都是O(n)的时间复杂度。

5. 双指针算法模板

for (int i = 0, j = 0; i < n; i ++ )
{
    while (j < i && check(i, j)) j ++ ;

    // 具体问题的逻辑
}
常见问题分类:
    (1) 对于一个序列,用两个指针维护一段区间
    (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
    关于双指针算法的模板,仅当做参考意义。因为双指针算法的应用十分广泛,模板不能代表所有情况,还是要具体问题具体分析。

6. 双指针算法例题

    第一个问题,假设有一个字符串,里面有很多单词,每个单词之间用空格隔开。接下来,请你用双指针算法来将每一个单词输出。
    这个问题是双指针算法最经典的应用。具体步骤如下:
        1. 首先,维护一个指针i,i指针每次都要确保指向每一个单词的起始位置。
        2. 其次,维护一个指针j,j刚开始也要指向起始位置,经过移动之后,j指针每次都要确保指向每一个单词的最后一个位置的下一个位置(空格)。
        3. i和j-1之间就是单词。
        4. 输出完单词之后,让i指向j+1,j指向i即可。之后重复以上操作即可。
#include <iostream>
#include <cstdio>
#include <string.h>
#include <string>

using namespace std;

int main(){
    char str[101];
    fgets(str,101,stdin);           //读入一整行
    int i,j;
    int length = strlen(str);
    for(int i=0;i<length;i++){
        //设定起始位置
        j = i;
        //移动j
        while(j < length && str[j] != ' '){
            j++;
        }
        //输出单词
        for(int k=i;k<j;k++){
            printf("%c",str[k]);
        }
        printf("\n");
        //输出单词之后调整i
        i = j;
    }
    return 0;
}

img

https://www.acwing.com/problem/content/801/
    这道题总体上难度较高,接下来采用双指针算法依次进行讲解。
    首先,我们需要了解双指针i和j的含义。i代表当前区间的右边界。j代表该区间满足连续不重复这个特点的最远的左边界。那么,i和j构成的区间是局部的最长连续不重复子序列。(不是全局,因为i不同)
    上述内容可能会不太容易理解,接下来我们会讲述实际的例子来辅助理解。
    例:1 2 2 3 5
    1.  初始,i和j都指向1,发现i和j所构成的区间没有重复的数,因此从局部上看构成了最长连续不重复子序列。(代表以i=1为终点(下标为0)的最长连续不重复子序列)长度为1。记录答案为1。
    2.  让i往后移动一个位置,此时i和j所构成的区间就是[1,2],我们也可以发现i和j所构成的区间没有重复的数,因此从局部上看也构成了最长连续不重复子序列,(代表以i=2为终点(下标为1)的最长连续不重复子序列)长度为2。但是,题目要求找到全局的最长连续不重复子序列。因此,我们要从局部当中进行比较,找到长度最大的,就是本题的答案。从第一步中,我们找到了一个局部的序列,长度为1。目前,我们找到了长度为2的局部的序列。因此,记录答案为2。
    3.  让i往后移动一个位置,此时i和j所构成的区间为[1,2,2],我们可以发现该区间存在重复的数,不满足题目的条件。因此,让j往后移动一个位置。
    4.  此时i和j所构成的区间[2,2],我们同样可以发现该区间存在重复的数,不满足题目的条件。因此,继续让j往后移动一个位置。
    5.  此时i和j所构成的区间[2],我们发现i和j所构成的区间没有重复的数,因此从局部上看构成了最长连续不重复子序列,(代表以i=2为终点(下标为2)的最长连续不重复子序列)长度为1。但是,目前从全局来看,最长的长度为2,因此记录答案仍是为2。
    6.  让i往后移动一个位置,此时i和j所构成的区间为[2,3],我们发现i和j所构成的区间不存在重复的数,因此从局部上看构成了最长连续不重复子序列,(代表以i=3为终点(下标为3)的最长连续不重复子序列)长度为2。记录答案为2。
    7.  让i继续往后移动一个位置,此时i和j所构成的区间为[2,3,5],我们发现i和j所构成的区间不存在重复的数,因此从局部上看构成了最长连续不重复子序列(代表以i=5为终点(下标为4)的最长连续不重复子序列),长度为3。记录答案为3。因此,本案例中的最长连续不重复子序列为[2,3,5]
长度为3。
    那么,根据上述的步骤,我们可以发现如下规律和问题:
        1. i,j指针始终向后移动。当i往后移动的时候,j为何只能向后移动,而不是向前移动?
        2. 如何判断i和j所构成的区间没有重复的数?
        3. 当i每次往后移动的时候,i和j所构成的区间就会增加一个数。当j每次往后移动的时候,i和j所构成的区间就会减少一个数。
        4. 如果i和j所构成的区间没有重复的数,当i往后移动一个位置时,如果该区间存在了重复的数,那么重复的数就是i所指向的数。
    接下来,对上述的问题进行讲解。
    1. 为什么,j只能向后移动?
        解答:我们可以证明,当i往后移动的时候,j只能向后移动,证明过程如下:
            假设,i和j构成了一个局部的最长连续不重复子序列。(i是一定大于等于j的),j始终代表满足连续不重复这个性质的最远的位置。(即:最长连续不重复性质)
            那么,当i往后移动的时候,假设当前i和j所构成的区间存在了重复的数。此时让j往前移动一个位置,发现i和j所构成的区间中的数增多了。此时,i和j所构成的区间就不满足连续不重复这个性质了。因此,j只能往后移动。
    2. 怎么判断i和j所构成的区间没有重复的数?
        我们可以用一个数组S,来动态存储每一个数所出现的次数。从上述的规律中我们可以发现,当i和j所构成的区间出现了重复的数时,重复的点就是i指向的数。因此,我们只需要判断当i指向的数所出现的次数大于1时,就代表发生了重复,否则就没有发生重复。当i每次往后移动的时候,i所指向的数所出现的次数都要+1。(代表区间增加了一个数)当j每次往后移动的时候,都要让j之前所指向的数所出现的次数-1。(代表区间减少了一个数)
#include <iostream>
#include <cstdio>
#include <cmath>

using namespace std;

int q[100010];
int s[100010];                          //代表动态计数的数组
    
int main(){
    int n;
    int res = 0;
    scanf("%d",&n);
    for(int i=0;i<n;i++){
        scanf("%d",&q[i]);
    }
    int i,j;
    for(i=0,j=0;i<n;i++){
        s[q[i]]++;                       //代表区间要增加一个数
        while(s[q[i]] > 1){              //代表目前区间有重复元素
            s[q[j]]--;                   //代表区间要移除一个数
            j++;                         //j向后移动
        }
        res = max(res,i-j+1);            //计算长度(全局)
    }
    printf("%d",res);
    return 0;
}
https://www.acwing.com/problem/content/description/802/
    二分法求解思路:
        1.  先遍历i,遍历i的过程中,利用二分法来求解。当i下标确定后,a[i]就是其中一个加数。由于b数列是一个递增数列,所以我们可以采用二分法进行求解。
    2.  如何进行二分呢?
        1. 假设a[i]已经确定,由于题目给定的条件就是a[i] + b[j] == x。因此,我们可以根据这个条件将b数组划分为两个区间。使得,左区间为:a[i] + b[mid] < x 而右区间为:a[i] + b[mid] >= x。其中,mid = (l+r) >> 1;
        2. 当mid位于左区间的时候,mid是取不到答案的,因此更新方式为:l = mid + 1;
        3. 当mid位于右区间的时候,mid可能会取到答案,因此更新方式为:r = mid;
        4. 如此反复,直到l == r 为止。此时,b[l]就是二分法所找到的答案,但未必是这个题目的答案。
        5. 因此,我们需要判断二分法所找到的答案b[l] + a[i] 是否等于x,如果等于就是本道题的答案,如果不等于那么就继续遍历i,继续二分,直到找到本题答案为止。(这道题保证了一定有解且唯一)
#include <iostream>
#include <cstdio>
using namespace std;

int main(){
    int n,m,x;
    int a[100010];
    int b[100010];
    scanf("%d %d %d",&n,&m,&x);
    for(int i=0;i<n;i++){
        scanf("%d",&a[i]);
    }
    for(int j=0;j<m;j++){
        scanf("%d",&b[j]);
    }
    for(int i=0;i<n;i++){
        int l = 0;
        int r = m;
        while(l < r){
            int mid = (l+r) >> 1;
            if(a[i] + b[mid] >= x){
                r = mid;
            }else{
                l = mid + 1;
            }
        }
        if(a[i] + b[l] != x){
            continue;
        }else{
            printf("%d %d",i,l);
        }
    }
    return 0;
}
    双指针法求解思路:
        1.  我们先让i指向第一个数组的第一个元素,让j指向第二个数组的最后一个元素。
        2.  对于每一个i,我们要寻找到a[i] + b[j] >= x的最小的j。
        3.  随着i增大,j一定是缩小的。因为i,j指向的数组都是升序的,i增大,i指向的数也增大,由于x是固定不变的,因此j一定会随着缩小。(换句话说:j只能往前移动)。
        4.  因此,根据上述的性质,我们可以构造出如下的判断条件:
            for(i=0,j=m-1;i<n;i++)
                while(j>=0 && a[i] + b[j] > x){
                    j--;
                }
                if(a[i] + b[j] == x){
                    输出下标
                    break;
                }
#include <iostream>
#include <cstdio>

using namespace std;

int A[100010];
int B[100010];

int main(){
    int n,m,x;
    scanf("%d %d %d",&n,&m,&x);
    for(int i=0;i<n;i++){
        scanf("%d",&A[i]);
    }
    for(int j=0;j<m;j++){
        scanf("%d",&B[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){
            printf("%d %d\n",i,j);
            break;
        }
    }
    return 0;
}
https://www.acwing.com/problem/content/2818/
#include <iostream>
#include <cstdio>

using namespace std;

int a[100010];
int b[100010];

int main(){
    int n,m;
    scanf("%d %d",&n,&m);
    for(int i=0;i<n;i++){
        scanf("%d",&a[i]);
    }
    for(int j=0;j<m;j++){
        scanf("%d",&b[j]);
    }
    int i = 0,j = 0;
    while(j<m && i<n){
        if(a[i] == b[j]){
            i++;
            j++;
        }else{
            j++;
        }
    }
    if(i == n){
        printf("Yes");
    }else{
        printf("No");
    }
    return 0;
}
    作者:gao79138
    链接:https://www.acwing.com/
    来源:本博客中的截图、代码模板及题目地址均来自于Acwing。其余内容均为作者原创。
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。