• 快速排序 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)。

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

      如下图:

                           

    快速排序的空间利用情况

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

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

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

    性能改善

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

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

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

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

     

  • 相关阅读:
    🔺 Garbage Remembering Exam UVA
    Cows and Cars UVA
    Probability|Given UVA
    Crossing Rivers HDU
    均匀分布和高斯分布
    Race to 1 UVA
    XMPPElementReceipt wait return,
    someone like you,
    第三方统计,
    截获的感觉,
  • 原文地址:https://www.cnblogs.com/mengdd/p/2790022.html
Copyright © 2020-2023  润新知