排序
插入排序
1 void InsertionSort(ElementType A[],int N) 2 { 3 int j,p; 4 ElementType Tmp; 5 for(p=1;p<N;p++)//N-1次循环 6 { 7 Tmp=A[p]; 8 for(j=p;j>0&&Tmp<A[j-1];j--) 9 A[j]=A[j-1]; 10 A[j]=Tmp; 11 } 12 }
由于嵌套循环每一次花费N次迭代,因此插入排序时间为O(N2);
逆序:指数组中具有性质i<j但A[i]>A[j]的序偶(A[i],A[j]);
逆序的个数正好是插入排序执行的交换次数。因为交换两个不安原序排列的相邻元素恰好消除一个逆序,而一个排过序的数组没用逆序。由此可以通过计算排列中的平均逆序数而得出插入逆序平均运行时间的精确的界。
定理:N个互异数的数组的平均逆序数是N(N-1)/4.
定理:通过交换相邻元素进行排序的任何算法平均需要O(N2)的时间。因为初始的平均逆序数是N(N-1)/4=O(N2),而且每次交换只减少一个逆序,因此需要O(N2)的交换。
希尔排序是一种增量排序方法,通过比较相距一定间隔的元素来工作。
1 //使用希尔增量的希尔排序 2 void Shellsort(ElementType A[],int N) 3 { 4 int i,j,Increment; 5 ElementType Tmp; 6 for(Increment=N/2;Increment>0;Increment/=2) 7 for(i=Increment;i<N;i++) 8 { 9 Tmp=A[i]; 10 for(j=i;j>=Increment;j-=Increment) 11 if(Tmp<A[j-Increment]) 12 A[j]=A[j-Increment]; 13 else 14 break; 15 A[j]=Tmp; 16 } 17 }
使用希尔排序最坏的时间复杂度是O(N2);
堆排序
建立N个元素的二叉堆,此时花费O(N)时间。然后我们执行N次的DeleteMin操作,按照顺序,最小的元素离开堆,通过将这些元素记录到第二个数组然后将给数组拷贝回来,得到N个元素的排序,由于每个DeleteMin的操作的耗时为O(logN),因此总的运行时间O(NlogN)。
该算法的主要问题是它使用了一个附加的数组,因此存储需求增加了一倍。
避免使用第二个数组方法是:在每次DeleteMin之后,堆缩小1,因此位于堆中的最后单元可以用来存放刚刚删去的元素,使用这种策略之后,该数组在最后一次deleteMin之后,以递减的顺序包含这些元素。
1 //堆排序 2 #define LeftChild(i) (2*(i)+1)//数组从0开始,先找左孩子 3 void PercDown(ElementType A[],int i,int N) 4 { 5 int Child; 6 ElementType Tmp; 7 for(Tmp=A[i];LeftChild(i)<N;i=Child) 8 { 9 Child=LeftChild(i); 10 if(Child!=N-1&&A[Child+1]>A[Child]) 11 Child++; 12 if(Tmp<A[Child]) 13 A[i]=A[Child]; 14 else 15 break; 16 } 17 A[i]=Tmp; 18 } 19 void Heapsort(ElementType A[],int N) 20 { 21 int i; 22 for(i=N/2;i>0;i--)//build Heap 23 PercDown(A,i,N); 24 for(I=N-1;i>0;i--) 25 { 26 Swap(&A[0],&A[i]);//Delete Max 27 PercDown(A,0,i); 28 } 29 }
定理:对N个互异项的随机排列进行堆排序,所用的比较平均次数为2NlogN-O(NlogN);
归并排序
合并两个已排序的表的时间是线性的,因为最多进行了N-1次比较,其中N是元素总数,每次比较都把一个元素加到最后的序列中,最后的比较除外,它至少添加2个元素;
1 //归并排序 2 void MSort(ElementType A[],ElementType TmpArray[],int Left,int Right) 3 { 4 int center; 5 if(Left<Right) 6 { 7 Center=(Left+Right)/2; 8 MSort(A,TmpArray,Left,Center); 9 MSort(A,TmpArray,Center+1,Right); 10 Merge(A,TmpArray,Left,Center+1,Right); 11 } 12 } 13 Mergesoft(ElementType A[],int N) 14 { 15 ElementType *TmpArray; 16 TmpArray=malloc(N*sizeof(ElementType)); 17 if(NULL==TmpArray) 18 FataError("out of space "); 19 else 20 { 21 MSort(A,TmpArray,0,N-1); 22 free(TmpArray); 23 } 24 } 25 void Merge(ElementType A[],ElementType TmpArray[],int Lpos,int Rpos ,int RightEnd) 26 { 27 int i,LeftEnd,NumElements,TmpPos; 28 LeftEnd=Rpos-1; 29 TmpPos=Lpos; 30 NumElements=RightEnd-Lpos+1; 31 while(Lpos<LeftEnd&&Rpos<RightEnd) 32 if(A[Lpos]<=A[Rpos]) 33 TmpArray[TmpPos++]=A[Lpos++]; 34 else 35 TmpArray[TmpPos++]=A[Rpos++]; 36 while(Lpos<LeftEnd)//左未完成 37 TmpArray[TmpPos++]=A[Lpos++]; 38 while(Rpos<RightEnd)//右未完成 39 TmpArray[TmpPos++]=A[Rpos++]; 40 //copy TmpArray to A[] 41 for(i=0;i<NumElements;i++,RightEnd--) 42 A[RightEnd]=TmpArray[RightEnd]; 43 }
归并排序是分析递归的典型事例
给运行时间写一个递归关系式:
对于N=1,归并排序所用时间为常数,记为1;
否则对N个数归并排序的用时等于完成两个大小为N/2的递归排序所用的时间加上合并的时间(N)。
T(1)=1
T(N)=2T(N/2)+N
解这个递归关系式就得所得结果;T(N)=NlogN+N;
虽然归并排序的运行时间是O(NlogN),但是它很难用于主存排序,问题在于合并两个排序的表需要额外的附加内存,在整个算法中还要花费时间将数据拷贝到临时存储,然后再拷贝回去,其结果放慢了排序速度。
快速排序
是在实践中最快的已知排序算法,它的平均运行时间是O(NlogN);该算法之所以特别快是因为非常精炼的和高度优化的内部循环;
选取枢纽元的方法:一种安全的方针是随机选取枢纽元(效率不高,代价大),二取三数中值分割的方法(实用)
1 //快速排序 2 ElementType Median3(ElementType A[],int Left,int Right) 3 { 4 int Center=(Left+Right)/2; 5 if(A[Left]>A[Center]) 6 Swap(&A[Left],&A[Center]); 7 if(A[Left]>A[Right]) 8 Swap(&A[Left],&A[Right]); 9 if(A[Center]>A[Right]) 10 Swap(&A[Center],&A[Right]); 11 Swap(&A[Center],&A[Right-1]); 12 return A[Right-1]; 13 } 14 #define Cutoff(3) 15 void Qsort(ElmentType A[],int Left,int Right) 16 { 17 int i,j; 18 ElementType Pivot; 19 if(Left+Cutoff<=Right)//保证数量比较多的元素 20 { 21 Pivot=Median3(A,Left,Right); 22 i=Left;j=Right-1; 23 for(;;) 24 { 25 while(A[i++]<Pivot){} 26 while(A[j--]<Pivot){} 27 if(i<j) 28 swap(&A[i],&A[j]); 29 else 30 break; 31 } 32 Swap(&A[i],&A[Right-1]);//枢纽交换 33 Qsort(A,Left,i-1); 34 Qsort(A,i+1,Right); 35 } 36 else 37 InsertionSort(A+Left,Right-Left+1);//元素数量少用插入排序 38 }
注意,将for循环改成如下是不能运行的,原因在晕若A[i]=A[j]=Privot则会产生一个死循环;
1 for(;;) 2 { 3 while(A[i]<Pivot) i++;//修改 4 while(A[j]<Pivot) j--; 5 if(i<j) 6 swap(&A[i],&A[j]); 7 else 8 break; 9 }
快排算法分析:
快速排序也是递归的,快排的运行时间等于两个递归调用的运行时间加上花费在分割上的线性时间;
T(0)=T(1)=1
T(N)=T(i)+T(N-i-1)+cN;
最坏情况之下:T(N)=T(1)+cΣi=O(N2);
最好情况之下:T(N)=cNlogN+N=O(NlogN);
证明排序的一般下界:
任何只用到比较的算法在最坏情况下需要O(NlogN)次比较。即使在平均情况下,只用到比较的排序算法都需要O(NlogN)次比较。
定理:令T是深度为d的二叉树,则T最多有2d个树叶;
→ 具有L片树叶的二叉树深度至少logL;
一般排序算法在最坏情况下的运行时间是O(NlogN),但是在某些特殊情况下以线性时间进行排序也是可以的;比如桶排序,因此排序的时候要广泛利用信息,仅仅比较浪费了信息量;
堆排序要比希尔排序慢,尽管它是一个带有明显紧凑内循环的o(NlogN)算法,对该算法的深入分析知道,为了移动数据,堆排序要进行2次比较。