快速排序 Quick Sort

快速排序 Quick Sort

快速排序的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

一趟快速排序(或一次划分)的过程如下:首先任意选取一个记录(通常可选第一个记录)作为枢轴(或支点)(pivot),然后按下列原则重新排列其余记录:将所有关键字比它小的记录都安置在它的位置之前,将所有关键字比它大的记录都安置在它的位置之后。

  经过一趟快速排序之后,以该枢轴记录最后所落的位置i作分界线,将序列分割成两个子序列,之后再分别对分割所得的两个子序列进行快速排序。

  可以看出这个算法可以递归实现,可以用一个函数来实现划分,并返回分界位置。然后不断地这么分下去直到排序完成,可以看出函数的输入参数需要提供序列的首尾位置。

快速排序的实现

划分实现1 (枢轴跳来跳去法)

  一趟快速排序的实现:设两个指针low和high,设枢轴记录的关键字为pivotkey,则首先从high所指位置起向前搜索找到第一个关键字小于pivotkey的记录和枢轴记录互相交换,然后从low所指位置起向后搜索,找到第一个关键字大于pivotkey的记录和枢轴记录互相交换,重复这两步直至low==high为止。

  下面的代码例子元素类型为int,并且关键字就是其本身。

Partition 实现1

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的位置才是枢轴元素的最终位置,所以可以先将枢轴元素保存起来,排序过程中只作元素的单向移动,直至一趟排序结束后再将枢轴元素移至正确的位置上。

  代码如下:

 

Partition 实现方法2

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;

}

 

可以看到减少了每次交换元素都要进行的三个赋值操作,变成了一个赋值操作。

  细节就是每次覆盖掉的元素都已经在上次保存过了,所以不必担心,而第一次覆盖掉的元素就是枢轴元素,最后覆盖在了它应该处于的位置。

 

递归形式的快速排序算法

 

Quick Sort

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)。

  原因:因为每次的枢轴都选择第一个元素,在有序的情况下,性能就蜕化了。

  如下图:

快速排序 Quick Sort_数据结构与算法


 

快速排序的空间利用情况

  从空间上看,快速排序需要一个栈空间来实现递归。

  若每一趟排序都将记录序列分割成长度相接近的两个子序列,则栈的最大深度为log2n+1(包括最外层参量进栈);但是,若每趟排序之后,枢轴位置均偏向子序列的一端,则为最坏情况,栈的最大深度为n。

  如果在一趟划分之后比较分割所得两部分的长度,且先对长度短的子序列中的记录进行快速排序,则栈的最大深度可降为O(logn)。

性能改善

  为改进快速排序算法,随机选取界点或最左、最右、中间三个元素中的值处于中间的作为界点,通常可以避免原始序列有序的最坏情况。

  然而,即使如此,也不能使快速排序在待排记录序列已按关键字有序的情况下达到O(n)的时间复杂度(冒泡排序可以达到)。

  为此,可以如下修改划分算法:在指针high减去1和low增加1的同时进行“起泡”操作,即在相邻两个记录处于“逆序”时进行互换,同时在算法中附设两个布尔型变量分别指示指针low和high在从两端向中间移动的过程中是否进行过交换记录的操作,若没有,则不需要对低端或高端子表进行排序,这将进一步改善快速排序的平均性能。

  另外,将递归算法改为非递归算法,也将加快速度,因为避免了进出栈和恢复断点等工作。