排序之快速排序詳解


一、算法介紹

快速排序(Quick Sort):它的基本思想是,通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,分別對這丙部分繼續進行快速排序,直至整個序列有序。

任取一個元素 (如第一個) 為中心
所有比它小的元素一律前放,比它大的元素一律后放,形成左右兩個子表;
對各子表重新選擇中心元素並依此規則調整,直到每個子表的元素只剩一個

①每一趟的子表的形成是采用從兩頭向中間交替式逼近法;

②由於每趟中對各子表的操作都相似,可采用遞歸算法。

二、基本步驟

設置兩個指針i,j,首先在序列里面選出一個樞紐temp出來,將j指向的數字和temp比較,如果比temp大,則減1,如果比temp小,應該把當前j指向的位置上面的數值和

三、算法分析

最好:划分后,左側右側子序列的長度相同,

最壞:從小到大排好序,遞歸樹成為單支樹,每次划分只得到一個比上一次少一個對象的子序列,必須經過 n-1 趟才能把所有對象定位,而且第 i 趟需要經過 n-i 次關鍵碼比較才能找到第 i 個對象的安放位置

若出現各種可能排列的概率相同,則可取最好情況和最壞情況的平均情況

時間效率:O(nlog2n) —每趟確定的元素呈指數增加
空間效率:O(log2n)—遞歸要用到棧空間
穩 定 性: 不穩定 —可選任一元素為支點

1.如何選樞紐

由上述描述可以知道,快速排序是以樞紐的點進行來回交換,所以快速排序的排序趟數和初始的序列有關系。
所以選擇快速排序的樞紐點是非常重要的,因為關系到排序的效率。

取前或后法:序列中的第一個或最后一個元素作為基准,如果輸入序列(上文中的數組)是隨機的,處理時間可以接受的。如果數組已經有序時,此時的分割就是一個非常不好的分割。因為每次划分只能使待排序序列減一,此時為最壞情況,時間復雜度為Θ(n^2)。而且,輸入的數據是有序或部分有序的情況是相當常見的。因此,使用第一個元素作為樞紐元是非常糟糕的

隨機選取基准
這是一種相對安全的策略。由於樞軸的位置是隨機的,那么產生的分割也不會總是會出現劣質的分割。在整個數組數字全相等時,仍然是最壞情況,時間復雜度是O(n2)。所以隨機化快速排序可以對於絕大多數輸入數據達到O(nlogn)的期望時間復雜度。

三數取中法:在快排的過程中,每一次我們要取一個元素作為樞紐值,以這個數字來將序列划分為兩部分。在此我們采用三數取中法,也就是取左端、中間、右端三個數,然后進行排序,將中間數作為樞紐值。顯然使用三數中值分割法消除了預排序輸入的不好情形,並且減少快排大約14%的比較次數。

2.如何證明時間復雜度

1、最優情況

在最優情況下,Partition每次都划分得很均勻,如果排序n個關鍵字,其遞歸樹的深度就為 [log2n]+1( [x] 表示不大於 x 的最大整數),即僅需遞歸 log2n 次,需要時間為T(n)的話,第一次Partiation應該是需要對整個數組掃描一遍,做n次比較。然后,獲得的樞軸將數組一分為二,那么各自還需要T(n/2)的時間(注意是最好情況,所以平分兩半)。於是不斷地划分下去,就有了下面的不等式推斷:
這說明,在最優的情況下,快速排序算法的時間復雜度為O(nlogn)。

2.最壞情況

然后再來看最糟糕情況下的快排,當待排序的序列為正序或逆序排列時,且每次划分只得到一個比上一次划分少一個記錄的子序列,注意另一個為空。如果遞歸樹畫出來,它就是一棵斜樹。此時需要執行n‐1次遞歸調用,且第i次划分需要經過n‐i次關鍵字的比較才能找到第i個記錄,也就是樞軸的位置,因此比較次數為n(n-1)/2,最終其時間復雜度為O(n^2)。

3.平均時間復雜度

直接設對規模的數組排序需要的時間期望為, 期望其實就是平均復雜度換個說法.
空表的時候不用排, 所以初值條件就是 T(0) = 0 .所謂快排就是隨便取出一個數,一般是第一個數,然后小於等於他的放左邊, 大於他的的排右邊.比如左邊 k 個那接下來還要排: T(n - k) + T (k - 1) 的時間.然后 k 多少那是不確定的, 遍歷 1~ n , 出現概率都是相等的. 另外分割操作本身也要時間 P(n) , 操作花費是線性時間 P(n) = cn , 這也要加進去, 所以一共是:

四、完整代碼示例

public class QuickSort {

    //任取一個元素 (如第一個) 為中心
    //所有比它小的元素一律前放,比它大的元素一律后放,形成左右兩個子表;
    //對各子表重新選擇中心元素並依此規則調整,直到每個子表的元素只剩一個
    //一趟排序過程后我們返回樞紐的位置
    int partition(int A[], int left, int right) {
        //選擇樞紐元素
        int p = A[left];
        while (left < right) {
            //如果尾指針位置的數比樞紐數要大,移動尾指針的位置,否則就把所指示的值給首指針的位置
            while (left < right && A[right] >= p) {
                --right;
            }
            A[left] = A[right];
            //如果首指針位置的數比樞紐數要小,移動首指針的位置,否則就把所指示的值給尾指針的位置
            while (left < right && A[left] <= p) {
                ++left;
            }
            A[right] = A[left];
        }
        //此時的首尾指針已經相等,把樞紐的值賦給首尾指針相等的位置即可
        A[left] = p;
        return left;
    }

    //快速排序的遞歸
    void Quick(int A[], int left, int right) {
        //定義一個樞紐的位置
        int pnode;
        if (left < right) {
            pnode = partition(A, left, right);
            Quick(A, left, pnode - 1);
            Quick(A, pnode + 1, right);
        }
    }

    public static void main(String[] args) {

    }

參考文章

https://www.jianshu.com/p/c8b1384238f7

https://www.cnblogs.com/chengxiao/p/6262208.html

https://blog.csdn.net/oohaha_123/article/details/26558363

https://www.zhihu.com/question/22393997/answer/406278523

https://www.cnblogs.com/onepixel/articles/7674659.html


歡迎關注個人技術公眾號:Coder辰砂


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM