上节分析了O(n^2)的算法,这节就分析O(nlgn)的算法-归并,快速和堆排序。
一:综述
O(nlgn) 的算法可以分为两大类,两者所用的技术差别较大。归并和快速排序采用的是分治策略,这两者相当于一个对称的过程,一个是自顶向上合并子问题,另一个则自上向下分解子问题。而堆排序利用堆这一数据结构元素间的特殊关系来排序一个序列,另外采用二叉树的方式组织数据使其效率大大提高。
二:分治策略排序算法
1.为什么使用分治?
在上节算法的分析中,不管是冒泡。选择还是插入都不适用于大规模的数据,因为数据一大,数据间比较,移动的次数也增大,这导致运行时间大大增加。自然而然就想到如何把大的问题分解成小问题,如果小问题计算代价小且和原问题相似,同时合并的代价也小。那用分治策略是优于直接对大问题进行处理的。而在排序中,一个大数据不断的分解这个处理过程代价小,直到只剩一个元素的时候,问题规模最小只要简单处理,合并过程虽然需要较多处理,但代价也小。所以将分治用于排序主要是解决三个处理过程,怎么将大的数据分成小数据?怎么对小数据进行处理?怎么将处理结果合并成最后的结果?
2.如何实现归并排序
归并排序:
怎么分解?归并排序是将n规模的序列分成n/2规模(n为偶数情况,奇数也类似),n/2分为n/4,......。直到序列长度为1,这个小问题直接求解,1长度的序列就已经排好序了。怎么合并?归并排序的合并过程需要较多处理,我们就专门分析一下这个过程。依照算法导论,我们也利用牌堆进行分析,假设只有两张牌,比较,小的放上面,大的放下面。如果是两堆牌呢?这两堆牌是通过前面合并得到,它们各自都已经排好序了。同样的比较牌堆的第一张,小的取出来放到另一堆去,然后再比较两牌堆顶的牌继续不断的把小的取出。当某堆牌取空时,将另一堆剩下的直接放到第三堆的底下,因为它们都排好序了,所以剩下的肯定比第三堆的都大。这个过程最多执行n次,比如当两堆牌刚好牌数一样,并且大小都是交替的。
#include <iostream> const int len=10; using namespace std; void merge(int *a,int p,int q,int r) { int n1=q-p+1,n2=r-q; int *L=new int[n1](); int *R=new int[n2](); for (int i=0;i<n1;i++) { L[i]=a[p+i]; } for (int j=0;j<n2;j++) { R[j]=a[q+j+1]; } int i=0,j=0,k=p; while (i<n1&&j<n2) { if (L[i]<=R[j]) { a[k++]=L[i++]; } else { a[k++]=R[j++]; } } while (i<n1) { a[k++]=L[i++]; } while (j<n2) { a[k++]=R[j++]; } delete [] R; delete [] L; } int main() { int a[len]={2,4,5,7,10,1,2,3,6,9}; merge(a,0,4,9); for(int i=0;i<len;i++) cout<<a[i]<<" "; cout<<endl; }
merge代码分析,传入的是指向数组的指针,和(p,q),(q+1,r)间的两个已经排好序的序列如,主程序所举的例子2,4,5,7,10。1,2,3,6,9。在子函数里,先新建两个动态数组来存放这两个序列。然后如果任一序列未到底时,就比较,并赋值给a。一个序列到底了,就将另一个未到底的直接复制到a下。
综上我们完成了合并的过程,分解呢?分解就是要确定q的值,一般我们将q设置为 ,它能够将A[p,r]分成n/2向下和向上取整个元素。(把p,r分别为偶数,奇数情况考虑下就可以证明)。如何一直分解直到只剩一个元素时排序并返回呢?这就要利用递归的方法了,函数不断的调用自身,分解成越来越小的子问题。
#include <iostream> const int len=5; using namespace std; void merge(int *a,int p,int q,int r) { int n1=q-p+1,n2=r-q; int *L=new int[n1](); int *R=new int[n2](); for (int i=0;i<n1;i++) { L[i]=a[p+i]; } for (int j=0;j<n2;j++) { R[j]=a[q+j+1]; } int i=0,j=0,k=p; while (i<n1&&j<n2) { if (L[i]<=R[j]) { a[k++]=L[i++]; } else { a[k++]=R[j++]; } } while (i<n1) { a[k++]=L[i++]; } while (j<n2) { a[k++]=R[j++]; } delete [] R; delete [] L; } void mergesort(int *a,int p,int r) { if (p<r) { int q=(p+r)/2; mergesort(a,p,q); mergesort(a,q+1,r); merge(a,p,q,r); } } int main() { int a[len]={25,15,4,30,7}; mergesort(a,0,4); for(int i=0;i<len;i++) cout<<a[i]<<" "; cout<<endl; } //main end
程序分析:
上图分析了程序的递归调用流程,我们就根据上图来分析,首先调用mergesort(a,0,4);,0<4,所以计算q=2.调用mergesort(a,0,2);0<2,q=1,调用mergesort(a,0,1);0<1,q=0,调用mergesort(a,0,0);0==0,递归返回,执行mergesort(a,p,q)的下一条语句mergesort(a,q+1,r)这时q=0,r=1。调用mergesort(a,1,1),1==1,调用返回,执行mergesort(a,q+1,r)的下一条语句merge(a,p,q,r)。此时p=0,q=0,r=1.调用merge(a,0,0,1)。这时返回至mergesort(a,0,2),q=1,调用mergesort(a,q+1,r),即mergesort(a,2,2),返回,调用merge(a,0,1,2)。于是左边这一半排序完成,开始右边的排序,其中q=2,r=4,调用mergesort(a,q+1,r),即mergesort(a,3,4)。同样的在这一半中左边调用mergesort(a,p,q),右边调用mergesort(a,q+1,r),merge,merge后就对数组排好序了。
递归的过程确实容易搞混淆的,在程序里是顺序的,而不是并行的两个排序过程。另外变量间的变化也是容易弄错的。
复杂度分析:
MERGE函数的复杂度为c •n (n为元素个数, c为移动一个元素, 比较一次所花费的工作量)。T(n)= 0, n=1 (只一个元素的数组无需排序),T(n)= 2 T(n/2) + c • n = , n>1。将T(n)展开,就的下图。
3.如何实现快速排序
如果说归并是从底不断排序好,不断合并,到顶时使序列最终成为有序,那么快速,就是从顶一直向下使序列不断有序,当到底时,序列也就成为有序了的。其中有序是确定一个值Aq把序列分为两个部分,一部分的所有值都比Aq大,另一部分都比Aq小。
数组A[p…r]被划分为两个(可能空)子数组A[p…q-1]和A[p+1..r],使得A[p…q-1]中每个元素都小于或等于A[q],A[q+1..r]中的元素大于等于A[q]。下标q在这个划分过程中进行计算;这个数组的划分过程很重要,通过交换来维护两个子区域的性质。
#include <iostream> const int len=8; using namespace std; int partition(int *a,int p,int r) { int x=a[r],i=p-1,temp=0; for (int j=p;j<r-1;j++) { if (a[j]<x) { i++; temp=a[i]; a[i]=a[j]; a[j]=temp; } } temp=a[i+1]; a[i+1]=a[r]; a[r]=temp; return i+1; } int main() { int p,a[len]={2,8,7,1,3,5,6,4}; p=partition(a,0,7); for(int i=0;i<len;i++) cout<<a[i]<<" "; cout<<endl; } //main end
如上图和程序,首先初始化x称为主元为最后一个元素a[r],i为序列开始之前的一个值,然后j从p开始的r-1,判断其是否小于等于主元,若满足则i加1,并交换a[i]和a[j]。这是为了满足当元素属于下面各个范围时,符合一定性质 1.p<=k<=i,a[k]<=x; 2. i+1<=k<=j-1,a[k]>x; 3.k=r,a[k]=x;初始时,p和i间,i+1和j-1间没有元素,成立,进入循环,假设第一个元素大于主元,j加1,什么都不做,符合上面的性质。如第一个元素小于主元,i加1,交换a[i]和a[j],先扩大i的范围,再将小于主元的元素交换进来。
整个快速排序的过程,也类似于归并排序,分解,解决,合并。分解就是确定主元在序列中位置,解决即递归用于两个数组,合并这无需,因为递归到最后,数组都排好序了。
#include <iostream> const int len=8; using namespace std; int partition(int *a,int p,int r) { int x=a[r],i=p-1,temp=0; for (int j=p;j<=r-1;j++) { if (a[j]<x) { i++; temp=a[i]; a[i]=a[j]; a[j]=temp; } } temp=a[i+1]; a[i+1]=a[r]; a[r]=temp; return i+1; } void quicksort(int *a,int p,int r) { int q; if (p<r) { q=partition(a,p,r); quicksort(a,p,q-1); quicksort(a,q+1,r); } } int main() { int a[len]={2,8,7,1,3,5,6,4}; quicksort(a,0,7); for(int i=0;i<len;i++) cout<<a[i]<<" "; cout<<endl; } //main end
算法分析(略)
算法导论专题一--排序算法(2)