内容:
- two pointers简介
- a+b=M
- 序列合并问题
- 归并排序
- 快速排序
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)
算法步骤:
- 令i=0,j=n-1;
- 如果A[i]+B[j]==M,即满足条件,同时右移i,左移就,即i++,j--;
- 如果A[i]+B[j]<M,无论如何移动j都无法使得2成立时,只能右移i使得2成立,即i++;
- 如果A[i]+B[j]>M,无论如何移动i都无法使得2成立时,只能左移j使得2成立,即j--;
- 反复判断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。
算法过程:
- 令i=0,j=0,index=0,i作为序列A的起始位,j作为B的起始位,index作为C的起始位;
- 如果A[i]<B[j],将A[i]加入序列C中,并让i右移一位,即i++;
- 如果A[i]>B[j],将B[i]加入序列C中,并让j右移一位,即j++;
- 如果A[i]==B[j],则任意一个加入序列C中,并对应下标加1;
- 注意:完成上述过程并不代表程序结束,两组序列存在一组提前加完的情况,即仍需要继续将剩余序列加入到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的幂次。
于是就有了这样的思路:
- 令步长step的初值为2;
- 然后将数组中的每step个元素作为一组,将其内部进行排序;
- 再令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]的元素放在左边;形成两个子序列,再将子序列重复上述操作,直到子序列规模很小时,也就是递归到底时,两边就会直接形成有序序列,整个序列自然而然也就是有序序列了。
那么我们第一步是要实现元素的划分
代码如下:
- 令临时变量temp保存arr[0],并令下标l和r指向序列左右两端,即l=0,r=n-1;
- 如果arr[r]>temp,r右移,直到不大于temp,将arr[r]的值赋给arr[l],执行下一步;
- 如果arr[l]<temp,l左移,直到不小于temp,将arr[l]的值赋给arr[r],执行上一步;
- 重复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是编程提高效率的一种重要途径!!!
不忘初心,方得始终!