快速排序 Quick Sort
快速排序 Quick Sort
快速排序的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
一趟快速排序(或一次划分)的过程如下:首先任意选取一个记录(通常可选第一个记录)作为枢轴(或支点)(pivot),然后按下列原则重新排列其余记录:将所有关键字比它小的记录都安置在它的位置之前,将所有关键字比它大的记录都安置在它的位置之后。
经过一趟快速排序之后,以该枢轴记录最后所落的位置i作分界线,将序列分割成两个子序列,之后再分别对分割所得的两个子序列进行快速排序。
可以看出这个算法可以递归实现,可以用一个函数来实现划分,并返回分界位置。然后不断地这么分下去直到排序完成,可以看出函数的输入参数需要提供序列的首尾位置。
快速排序的实现
划分实现1 (枢轴跳来跳去法)
一趟快速排序的实现:设两个指针low和high,设枢轴记录的关键字为pivotkey,则首先从high所指位置起向前搜索找到第一个关键字小于pivotkey的记录和枢轴记录互相交换,然后从low所指位置起向后搜索,找到第一个关键字大于pivotkey的记录和枢轴记录互相交换,重复这两步直至low==high为止。
下面的代码例子元素类型为int,并且关键字就是其本身。
typedef int ElemType; int Patition(ElemType A[], int low, int high) { ElemType pivotkey=A[low]; ElemType temp; while(low<high) { while(low <high && A[high]>=pivotkey) { --high; } temp=A[high]; A[high]=A[low]; A[low]=temp; while(low<high && A[low]<=pivotkey) { ++low; } temp=A[high]; A[high]=A[low]; A[low]=temp; } return low; }
划分实现2 (枢轴一次到位法)
从上面的实现可以看出,枢轴元素(即最开始选的“中间”元素(其实往往是拿第一个元素作为“中间”元素))在上面的实现方法中需要不断地和其他元素交换位置,而每交换一次位置实际上需要三次赋值操作。
实际上,只有最后low=high的位置才是枢轴元素的最终位置,所以可以先将枢轴元素保存起来,排序过程中只作元素的单向移动,直至一趟排序结束后再将枢轴元素移至正确的位置上。
代码如下:
int Patition(ElemType A[], int low, int high) { ElemType pivotkey=A[low]; ElemType temp = A[low]; while(low<high) { while(low <high && A[high]>=pivotkey) { --high; } A[low]=A[high]; while(low<high && A[low]<=pivotkey) { ++low; } A[high]=A[low]; } A[low] = temp; return low; }
可以看到减少了每次交换元素都要进行的三个赋值操作,变成了一个赋值操作。
细节就是每次覆盖掉的元素都已经在上次保存过了,所以不必担心,而第一次覆盖掉的元素就是枢轴元素,最后覆盖在了它应该处于的位置。
递归形式的快速排序算法
void QuickSort(ElemType A[], int low, int high) { if(low<high) { int pivotloc=Patition(A,low, high); QuickSort(A, low, pivotloc-1); QuickSort(A, pivotloc+1, high); } }
不管划分是上面哪一种实现,都可以用这个递归形式进行快速排序。
需要注意的是这个if语句不能少,不然没法停止,会导致堆栈溢出的异常。
快速排序的性能分析
时间复杂度
快速排序的平均时间为Tavg(n)=knln(n),其中n为待排序列中记录的个数,k为某个常数,在所有同数量级的先进的排序算法中,快速排序的常数因子k最小。
因此,就平均性能而言,快速排序是目前被认为是最好的一种内部排序方法。通常认为快速排序在平均情况下的时间复杂度为O(nlogn)。
但是,快速排序也不是完美的。
若初始记录序列按关键字有序或基本有序,快速排序将蜕化为冒泡排序,其时间复杂度为O(n2)。
原因:因为每次的枢轴都选择第一个元素,在有序的情况下,性能就蜕化了。
如下图:
快速排序的空间利用情况
从空间上看,快速排序需要一个栈空间来实现递归。
若每一趟排序都将记录序列分割成长度相接近的两个子序列,则栈的最大深度为log2n+1(包括最外层参量进栈);但是,若每趟排序之后,枢轴位置均偏向子序列的一端,则为最坏情况,栈的最大深度为n。
如果在一趟划分之后比较分割所得两部分的长度,且先对长度短的子序列中的记录进行快速排序,则栈的最大深度可降为O(logn)。
性能改善
为改进快速排序算法,随机选取界点或最左、最右、中间三个元素中的值处于中间的作为界点,通常可以避免原始序列有序的最坏情况。
然而,即使如此,也不能使快速排序在待排记录序列已按关键字有序的情况下达到O(n)的时间复杂度(冒泡排序可以达到)。
为此,可以如下修改划分算法:在指针high减去1和low增加1的同时进行“起泡”操作,即在相邻两个记录处于“逆序”时进行互换,同时在算法中附设两个布尔型变量分别指示指针low和high在从两端向中间移动的过程中是否进行过交换记录的操作,若没有,则不需要对低端或高端子表进行排序,这将进一步改善快速排序的平均性能。
另外,将递归算法改为非递归算法,也将加快速度,因为避免了进出栈和恢复断点等工作。