《算法笔记》——第四章two pointers 学习记录

two pointers

two pointers是算法编程中一种非常重要的思想,但是很少会有教材单独拿出来讲,其中一个原因是它更倾向于是一种编程技巧,而长得不太像是一个“算法”的模样。two pointers的思想十分简洁,但却提供了非常高的算法效率,下面就来一探究竟。

以一个例子引入:给定一个递增的正整数序列和一个正整数M,求序列中的两个不同位置的数a和b,使得它们的和恰好为M,输出所有满足条件的方案。例如给定序列{1,2,3, 4,5,6}和正整数M=8,就存在2+6=8与3+5=8成立。

本题的一个最直观的想法是,使用二重循环枚举序列中的整数a和b,判断它们的和是否为M,如果是,输出方案;如果不是,则继续枚举。

显然,这种做法的时间复杂度为\(O(n^2)\),对n在\(10^5\)的规模时是不可承受的。来看看高复杂度的原因是什么:

  1. 对一个确定的a[i]来说,如果当前的a[j]满足a[i] + a[j]> M,显然也会有a[i] + a[j+1]> M成立(这是由于序列是递增的),因此就不需要对a[j]之后的数进行枚举。如果无视这个性质,就会导致j进行了大量的无效枚举,效率自然十分低下。
  2. 对某个a[i]来说,如果找到一个a[j],使得a[i] + a[j]> M恰好成立,那么对a[i+1]来说,一定也有a[i+1] + a[j]> M成立,因此在a[i]之后的元素也不必再去枚举。

上面两点似乎体现了一个问题:i和j的枚举似乎是互相牵制的,而这似乎可以给优化算法带来很大的空间。事实上,本题中two pointers将利用有序序列的枚举特性来有效降低复杂度。它针对本题的算法过程如~下:

令下标i的初值为0,下标j的初值为n-1,即令i、j分别指向序列的第一个元素和最后一个元素,接下来根据a[i] + a[j]与M的大小来进行下面三种选择,使i不断向右移动、使j不断向左移动,直到i≥j 成立。

  1. 如果满足a[i]+a[j]==M,说明找到了其中一组方案。由于序列递增,不等式a[i+1]+a[j]>M与a[i]+a[j-1]<M均成立,但是a[i+1]+ a[j-1]与M的大小未知,因此剩余的方案只可能在[i+ 1,j- 1]区间内产生,令i=i+1、j=j-1 (即令i向右移动,j向左移动)。
  2. 如果满足a[i]+a[j]>M,由于序列递增,不等式a[i+1]+a[j]> M成立,但是a[i] +a[j-1]与M的大小未知,因此剩余的方案只可能在[i,j- 1]区间内产生,令j=j-1 (即令j向左移动)。
  3. 如果满足a[i]+a[j]<M,由于序列递增,不等式a[i]+a[j-1]<M 成立,但是a[i+ 1]+a[j]与M的大小位置,因此剩余的方案只可能在[i+ 1, j]区间内产生,令i=i+1 (即令i向右移动)。

反复执行上面三个判断,直到\(i\gej\)成立,代码如下:

while(i<j)
{
    if(a[i]+a[j] == m)
    {
        printf("%d %d\n",i,j);
        i++,j--;
    }
    else if(a[i]+a[j]<m)
        i++;
    else
        j--;
}

下面来分析下算法的复杂度。由于i的初值为0,j的初值为n-1,而程序中变量i只有递增操作、变量j只有递减操作,且循环当i≥j时停止,因此i和j的操作次数最多为n次,时间复杂度为\(O(n)\)。可以发现,two pointers的思想充分利用了序列递增的性质,以很浅显的思
想降低了复杂度。

序列合并

再来看序列合并问题。假设有两个递增序列A与B,要求将它们合并为一个递增序列C。

同样的,可以设置两个下标i和j,初值均为0,表示分别指向序列A的第一个元素和序列B的第一个元素,然后根据A[i]与B[j]的大小来决定哪一个放入序列C。

  1. 若A[i]<B[j],说明A[i]是当前序列A与序列B的剩余元素中最小的那个,因此把A[i]加入序列C中,并让i加1 (即让i右移一位)。
  2. 若A[i]>B[j],说明B[j]是当前序列A与序列B的剩余元素中最小的那个,因此把B[j]加入序列C中,并让j加1 (即让j右移一位)。
  3. 若A[i]== B[j],则任意选一个加入到序列C中,并让对应的下标加1。

上面的分支操作直到i、j中的一个到达序列末端为止,然后将另一个序列的所有元素依次加入序列C中,代码如下:

int merge(int a[],int b[],int c[],int n,int m)
{
    int i=0,j=0,index=0;
    while(i<n && j<m)
    {
        if(a[i] <= b[j])
            c[index++]=a[i++];
        else
            c[index++]=b[j++];
    }
    while(i<n) c[index++]=a[i++];
    while(j<m) c[index++]=b[j++];
    return index;
}

最后,一定有读者问,two pointers到底是怎样的一种思想?事实上,two pointers最原始的含义就是针对本节第一个问题而言的,而广义上的two pointers则是利用问题本身与序列的特性,使用两个下标i、j对序列进行扫描(可以同向扫描,也可以反向扫描),以较低的复杂度(一般是\(O(n)\)的复杂度)解决问题。读者在实际编程时要能够有使用这种思想的意识。

posted @ 2021-02-10 17:53  Dazzling!  阅读(28)  评论(0编辑  收藏  举报