数据结构——排序(二)

归并排序

  归并排序最坏情况下时间复杂度为O(NlogN),它所使用的比较次数几乎是最优的。同时归并排序是分治策略和递归策略的一个非常好的实例。归并排序的基本操作是合并两个已经排序的表。对于一个要排序的数组,归并算法首先将数组分为两部分,然后分别对这两部分递归调用自己,这个递归算法的递归终止条件是数组长度为一,此时结束递归然后返回,算法执行合并两个数组的操作。合并操作也比较简单,最简单的方式是使用3个数组A,B,C。其中A和B是要合并的数组,C是合并后的数组。算法遍历A和B,从A和B中选择较小的元素放到C中,直到A和B中都没有元素为止。下面给出一个例程:

//驱动例程

void Mergesort(ElementType A[],int N)

{

       ElementType *TmpArray;

       TmpArray=malloc(N*sizeof(ElementType));

If(TmpArray!=Null)

{

       Msort(A,TmpArray,0,N-1);

Free(TempArray);

}

else

{

       ERROR();

}

}

//递归调用的例程

void Msort(ElementType A[],ElementType TmpArray[],int Left,int Right)

{

       int Center;

       if(Left<Right)    //递归终止条件

{

       Center=(Left+Right)/2;

       Msort(A,TmpArray,Left,Center);

       Msort(A,TmpArray,Center+1,Right);

       Merge(A,TmpArray,Left,Center+1,Right);

}

}

  其中的关键在于Merge函数,这个函数负责合并两个已排序数组,这个函数的精妙之处在于使用了一个大的活动数组作为临时数组存储排序数字,而不是使用很多小的局部数组作为C数组,这样对于小内存时比较适用,例程如下:

void Merge(ElementType A[],ElementType TmpArray[],int Lpos,int Rpos,int RightEnd)

{

       int i,LeftEnd,NumElements,TmpPos;

       //进行合并要使用的变量

       LeftEnd=Rpos-1;

       TmpPos=Lpos;

       NumElements=RightEnd-Lpos+1;

       /*主循环*/

       while(TmpPos<=LeftEnd&&Rpos<=RightEnd)

              if(A[Lpos]<=A[Rpos])

                     TmpArray[TmpPos++]=A[Lpos++];

              Else

                     TmpArray[TmpPos++]=B[Rpos++];

       //如果还有没有放到TmpArray中的元素放到临时数组中

       while(Lpos<=LeftEnd)

              TmpArray[TmpPos++]=A[Lpos++];

       While(Rpos<=RightEnd)

              TmpArray[TmpPos++]=A[Rpos++];

       //将临时数组中的数据拷贝回待排序数组

       for(i=0;i<NumElements;RightEnd--,i++)

              A[RightEnd]=TmpArray[RightEnd];

}

       归并排序就是将问题分为小的问题,先解决小的问题,然后将小问题的结果合并到一起。在外排序中,归并排序的思想就是外排序思想的基石。

快速排序

  快速排序是目前已知的速度最快的算法,它的平均运行时间是O(NlogN),最坏情况下的时间复杂度为O(N*N),但是这种最坏情况可以很容易的避免。虽然理论上快速排序的时间复杂度并不比归并排序少,但在实际运行过程中它的速度高于归并排序。首先研究一下快速排序的基本思路(要排序的数组为S):

  1. 如果S中元素个数是0或者1,则返回。
  2. 取S中任一元素v,称之为枢纽元。
  3. 将S中剩余元素分为两个不相交的集合S1={x属于S,且x<=v}和S2={x属于S,且x>=v}。
  4. 返回{quicksort(S1),继随v,继而quicksort(S2)}。

  其中第3步中枢纽元的选择是算法的关键,这个选择导致了算法的时间复杂度的区别,好的选择可以是S1与S2保持均衡,类似与二叉查找树中保持平衡一样。一种好的选择是三数中值分割法,选择待排序数组中的最左边最右边和中间的数据元素的中值。这种选择方法一般就可以保证快速排序的快速执行。在确定枢纽元后,就需要对数组进行分割(将不同大小的数据元素放到不同的集合中),分割数组有很多种方式,这里描述一种比价简便可行的分割方法:首先,将选择的枢纽元与最后位置的元素进行互换,将枢纽元放到数组的最后位置。然后从数组的开始位置和枢纽元的前一个位置开始,将数组中的元素进行分割,将枢纽元排除到分割的外面,在分割的最后对枢纽元进行处理。设开始位置为i,枢纽元前一个位置为j,则i向数组后面移动,j向数组前面移动,i在遇到大于枢纽元的数值时停止,j在遇到小于枢纽元的数值时停止,如果i<j,则交换i和j处的数值。然后继续这一流程,直到i>j。最后将i所指位置的元素与枢纽元交换位置。经过这样分割后,在枢纽元左侧的元素都小于枢纽元,右边的都大于枢纽元。值得注意的是在遇到与枢纽元相等的元素时i与j是否应该停止。这种情况下,i于j应该有相同的行为,否则分割出的两个子数组会产生倾斜。经分析可知,如果i和j在遇到相同元素时不停止,则会产生不平衡的划分,如果停止产生的划分较为均衡,但是会产生更多的交换。停止操作导致的时间复杂度为O(nlogn),不平衡的时间复杂度为O(N*N)。因此选择在遇到与枢纽元相等的元素时停止的机制。需要注意的是,当数组很小时,快速排序的运行速度小于插入排序,原因在于快速排序使用到了递归,导致更长的时间。因此在数组较小时(N<=10)应该选择插入排序。下面研究下快速排序的例程:

void Quicksort(ElementType A[],int N)

{

   Qsort(A,0,N-1);

}

       这个是快速排序算法的驱动例程,主要负责调用真正的快排算法,并向快排算法中传递要排序的数组和数组的左端和右端,Qsort算法主要负责选取枢纽元和分割数据元素的工作。其中选取枢纽元使用三数中值分割法,具体例程如下:

       ElementType Median3(ElementType A[],int Left,int Right)

{

       int Center=(left+right)/2;

       if(A[left]>A[Center])

              swap(&A[left],&A[center]);

       if(A[left]>A[Right])

              swap(&A[left],&A[right]);

       if(A[center]>A[right])

              swap(&A[center],&A[right]);

       swap(&A[center],&A[Right-1]);

return A[Right-1];

}

  从例程可以看出,这个例程不仅返回了枢纽元,而且将进行判断的三个数字进行了交换,将最小的元素放到最左边,最大的元素放到最右边,枢纽元放到最大元素的左边。这样在后续的数据元素分割过程中,可以不用从最左边和最右边的位置开始判断,而是从最左边元素的右边元素和枢纽元的左边元素开始判断,减少进行的判断次数,实际上,这一优化极大地减少了运行时间。下面研究下Qsort排序算法的例程:

#define Cutoff (3)

void Qsort(ElementType A[],int Left,int Right)

{

       int i,j;

       Element Pivot;

If(Left+Cutoff<=Right)

{

       Pivot=Median3(A,Left,Right);

       i=Left;                               //最左边的位置

       j=Right-1;                         //枢纽元的位置

       for(;;)

{

//先递增或者递减,使得判断从最小元素的下一个元素和枢纽元的上一个位置开始

       while(A[++i]<Pivot){}

       while(A[--j]>Pivot){}

if(i<j)

       swap(&A[i],&A[j]);

else

       break;

}

Swap(&A[i],&A[Right-1]);//将枢纽元换到中间位置处

Qsort(A,Left,i-1);

Qsort(A,i+1,Right);

}

else       //排序元素过少时,使用插入排序(也是一个递归终止条件)

{

       InsertionSort(A+left,Right-Left+1);

}

}

       分析可知,快速排序最坏情况下的时间复杂度为O(n*n),平均情况和最好情况下时间复杂度都为O( NlogN)。可以将快速排序法稍加改造应用到选择问题上。

其他

  当对大型结构排序时,在需要交换时,数据的交换是非常昂贵的操作,这种情况可以使用指针来进行交换,从而减少数据交换的昂贵操作,称为间接排序。另外,在基于比较的排序算法中,利用决策树可以证明,基于比较的排序算法时间复杂度的下界为O(NlogN)。在特殊情况下,排序算法的时间复杂度可以降低到线性时间复杂度,最为典型的是桶排序。这样的排序需要额外的信息,要排序的数据A1到An必须小于M,则建立一个大小为M的数组Count并且初始化为0,读入A数组后,每个Ai都执行Count[Ai]++。这样读完A后,扫描Count输出排序后的数组,桶排序的时间复杂度为O(M+N)。另外,当要排序的数据无法全部读入内存的情况下,需要使用外排序的算法,简单来说就是使用归并排序的思想,首先排序几块,然后将每一个排序后的块结果合并起来。

posted @ 2016-03-23 22:19  libs5510  阅读(210)  评论(0编辑  收藏  举报