修炼算法内功——two pointers

内容:

  1. two pointers简介
  2. a+b=M
  3. 序列合并问题
  4. 归并排序
  5. 快速排序

1、two pointers简介

 two pointers是算法编程中一种非常重要的思想,它更倾向于一种编程技巧,为我们编程提供了非常高的算法效率。

2、a+b=M

以一个例子引入:给定一个递增的正整数序列和一个正整数M,求序列中的两个不同位置的数a和b,使得他们的和恰好为M,输出所有满足条件的方案。

最直接的方法:

使用二重循环枚举序列A和B,找到这样的a和b,使他们的和为M。

for(int i=0;i<n;i++){
  for(int j=i+1;j<n;j++){
    if(A[i]+B[j]==M){
       printf("%d+%d\n",A[i],B[j]);
    }
  }
}

这种做法的时间复杂度是O(n^2)级别的,对于我们算法要求是完全不够的。

复杂度高的原因: 

对于已经确定的A[i],如果满足A[i]+B[j]>M,显然也会有A[i]+B[j+1]>M的,这是因为这是个递增序列,即无论如何增加j都是无法得到和为M了,导致j进行了大量的无效枚举。

使用two pointers降低复杂度到O(n)

算法步骤:

  1. 令i=0,j=n-1;
  2. 如果A[i]+B[j]==M,即满足条件,同时右移i,左移就,即i++,j--;
  3. 如果A[i]+B[j]<M,无论如何移动j都无法使得2成立时,只能右移i使得2成立,即i++;
  4. 如果A[i]+B[j]>M,无论如何移动i都无法使得2成立时,只能左移j使得2成立,即j--;
  5. 反复判断2、3、4直到i>=j;

 代码如下:

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

 时间复杂度为O(n),显然,相对第一种做法大大优化了算法效率。可以发现,two pointers的思想充分利用了序列递增的性质,以很浅显的思想降低了复杂度。

3、序列合并问题

 假如有两个递增序列A和B,要求它们合并为一个递增序列C。

算法过程:

  1. 令i=0,j=0,index=0,i作为序列A的起始位,j作为B的起始位,index作为C的起始位;
  2. 如果A[i]<B[j],将A[i]加入序列C中,并让i右移一位,即i++;
  3. 如果A[i]>B[j],将B[i]加入序列C中,并让j右移一位,即j++;
  4. 如果A[i]==B[j],则任意一个加入序列C中,并对应下标加1;
  5. 注意:完成上述过程并不代表程序结束,两组序列存在一组提前加完的情况,即仍需要继续将剩余序列加入到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[i++];
     return index;
}

4、归并排序

归并排序的思想其实很简单,概括说来,就是“二分排序,有序合并”。将一个序列二分为两个子序列,让这两个子序列去完成各自的排序工作后再合并为一个序列;同时这个子序列也是可以执行同样的操作,再将自己二分为两个规模更小的子序列,排序再合并,直到规模为1是结束。

(1)、归并排序的递归实现——2—路归并排序

排序函数实现:

int mergeSort(int arr[],int l,int r){//传入数组,左边第一个元素和右边最后一个元素
    if(l<r){//递归结界
      int mid=(l+r)/2;
      mergeSort(arr,l,mid);//左子序列再二分排序合并
      mergeSort(arr,mid+1,r);//右子序列再二分排序合并
      merge(arr,l,mid,r);//子序列完成排序后合并
    }
}

 合并已经排序完成的序列操作实现:

void merge(int arr[],int l,int mid,int r){
  int i=l,j=mid+1;
  int temp[maxn],index=0;
  while(i<mid&&j<r){
    if(arr[i]<=arr[j])
        temp[index++]=arr[i++];
    else
        temp[index++]=arr[j++];
  while(i<mid)
    temp[index++]=arr[i++];
  while(j<r)
    temp[index++]=arr[j++];
  for(int k=0;k<index;k++){
    arr[l+k]=temp[k];
  }
}
        

(2)、非递归实现

2—路归并排序的非递归实现主要考虑到这样一点:每次分组时组内元素个数上限都是2的幂次。

于是就有了这样的思路:

  1. 令步长step的初值为2;
  2. 然后将数组中的每step个元素作为一组,将其内部进行排序;
  3. 再令step乘以2,重复上面操作,直到step/2超过元素个数n。

 代码如下: 

void mergeSort(int arr[]){
    for(int step=2;step/2<=n;step*=2){
        for(int i=1;i<=n;i+=step){
           int mid=i+step/2-1;
           if(mid+1<=n){
              merge(arr,i,mid,min(i+step-1,n));
           }
        }
    }
}

当然,如果题目只要求给出归并排序每一趟结束时的序列,那么完全可以使用sort函数来代替merge函数(只要时间限制允许),如下所示:

sort(arr+i,arr+min(i+step,n+1)); 

5、快速排序

快速排序的算法思想是,记首元素地址arr[0],将后面的元素逐一划分,大于arr[0]的元素放在右边,小于arr[0]的元素放在左边;形成两个子序列,再将子序列重复上述操作,直到子序列规模很小时,也就是递归到底时,两边就会直接形成有序序列,整个序列自然而然也就是有序序列了。

那么我们第一步是要实现元素的划分

 

 代码如下:

 

  1. 令临时变量temp保存arr[0],并令下标l和r指向序列左右两端,即l=0,r=n-1;
  2. 如果arr[r]>temp,r右移,直到不大于temp,将arr[r]的值赋给arr[l],执行下一步;
  3. 如果arr[l]<temp,l左移,直到不小于temp,将arr[l]的值赋给arr[r],执行上一步;
  4. 重复23直到l和r相遇,最后把temp放到相遇的位置即可。
void partition(int arr[],int l,int r){
   int temp=arr[l];
   while(l<r){
      while(l<r&&arr[r]>temp)
            r--;             //反复左移r
      arr[l]=arr[r];
      while(l<r&&arr[l]<temp)
            l++;             //反复右移l
      arr[r]=arr[l];
   }
arr[left]=temp;//temp中元素回位
return l;//返回相遇位置
}

 第二步是实现快速排序

void quickSort(int arr[],int l,int r){
    if(r>l){
      int pos=Partition(arr,l,pos-1);
      quickSort(arr,l,pos-1);
      quickSort(arr,pos+1,r);
    }
}

快速排序再最坏时时间复杂度是O(n^2),即当序列接近有序时。

总结:应用好two pointers是编程提高效率的一种重要途径!!!

 

 

不忘初心,方得始终!

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值