數據結構——快速排序


系列文章:數據結構與算法系列——從菜鳥到入門

描述

快速排序是基於分治模式的,下面按分治模式來進行分析:

分解:

  數組 A[p..r]被划分成兩個(可能空)子數組,A[p..q-1]和 A[q+1..r],使得 A[p..q-1]中的每個元素都小於等於 A(q),也小於等於 A[q+1..r]中的元素。

解決:

  遞歸的對子數組 A[p..q-1]和 A[q+1..r]進行再排序。

合並:

  子數組是就地排序,不需要合並操作,整個數組 A[p..r]已有序。

偽代碼:

quickSort(int[] A, int left, int right) {
    if (left >= right) {
        return;
    }
    int res = partition(A, left, right); // 划分區間,返回中位數
    quickSort(A, left, res-1); // 遞歸划分后的左區間
    quickSort(A, res+1, right); // 遞歸划分后的右區間
}

數組的划分過程(PARTITION)

PARTITION 過程:

  它對子數組 A[p..r]進行就地重排序。

偽代碼:

方法1.單向掃描

定義臨時變量 j,用於遍歷待排序數組,將小於等於 res 的 A[j]與 A[i]交換,這里的臨時變量 i 就是保存下次交換的位置。最終小於等於 res 的元素被全部交換到了數組的前面,操作的最后,將 res 交換到數組中最后一個小於等於它的元素的后面。這樣數組就分為了三個子區間,小於等於 res 區間,res 本身區間,大於 res 區間。接着,對非 res 本身的另兩個區間進行遞歸地同樣操作。

private static int partition1(int[] A, int left, int right) {
    int res = getStandard(A, left, right); // 獲取基准位
    int i = left;
    int num = A[res];
    swap(A, right, res);
    for (int j = left;j < right;j++) {
        if (A[j] <= num) {
            swap(A, j, i++);
        }
    }
    swap(A, right, i);
    return i;
}

 方法2.雙向掃描

詳情看這里:快速排序圖文說明,不再重造輪子。網上博客大部分都是這樣寫的。

private static int partition2(int[] A, int left, int right) {
    int l = left;
    int r = right;
    int res = A[left];
    while (l < r) {
        while (l<r && A[r]>res) {
            r--;
        }
        if (l < r) {
            A[l++] = A[r];
        }
        while (l<r && A[l]<res) {
            l++;
        }
        if (l < r) {
            A[r--] = A[l];
        }
        A[l] = res;
    }
    return l;
}

性能分析

快速排序的平均比較次數為 O(nlgn),時間復雜度為 O(nlgn)。

最壞情況:

最壞情況是,當序列已排序時,如:

{1,2,3,4,5,6,7,8}

選取序列的第一個值作為基准值,分成的兩個子序列長度為 1 與 n-1:

1,{2,3,4,5,6,7,8}

這樣必須經過 n-1 趟才能完成排序。因此,總的比較次數為:

所以最壞的時間復雜度為O(n^2)

最佳情況:

在最好情況下,每次划分所取得基准都是當前無序區的“中值”記錄,划分的結果是基准的左、右兩個無序區間的長度大致相等。總的關鍵字比較次數為:O(nlgn)

下面從遞歸樹的角度再分析,每次划分后,左、右子區間長度大致相等,根據二叉樹的性質,遞歸樹的高度為 O(lgn),而遞歸樹每一層上各節點對應的划分過程中所需要的關鍵字比較次數總和不超過 n,故整個排序過程所需要的關鍵字比較總次數為 O(nlogn)

空間復雜度:

快速排序在系統內部需要一個棧來實現遞歸。若每次划分較為均勻,則其遞歸樹的高度為 O(lgn),故遞歸后需棧空間為 O(lgn)。最壞的情況,遞歸樹的高度為 O(n),所需的棧空間為 O(n)。

穩定性:

快速排序是非穩定的。

基准的選取

在當前無序區中選取划分的基准關鍵字是決定算法性能的關鍵。以下兩種方式都是為了優化,選取固定基准所產生的最壞情況。

“三者取中”:

即在當前區間里,將該區間首、尾和中間位置上的關鍵字比較,取三者的中值所對應的記錄作為基准。

private static int getStandard(int[] A, int left, int right) {
   // 三者取中(直接將三者交換排序,返回中間位置)
    int middle = left+(right-left)/2;
    int a = A[left],b = A[right],c = A[middle];
    if (a > b) {swap(A, left, right);}
    if (a > c) {swap(A, left, middle);}
    if (b > c) {swap(A, right, middle);}
    int res = middle;
    return res;
}

“隨機選擇”:

取位於 low 和 high 之間的隨機數 k(low<=k<=high),用 R[k] 作為基准,這相當於強迫 R[low....high] 中的記錄是隨機分布的。

private static int getStandard(int[] A, int left, int right) {
    // 選取基准關鍵字 —— 取隨機數
    int res = new Random().nextInt(right-left)+left;
    return res;
}

參考資料

[1] 數據結構(Java版), 3.3.2 - 快速排序

[2] 數據結構與算法分析——Java語言描述, 7.3.2 - 快速排序

[3] 算法導論, 7.4 - 快速排序分析


免責聲明!

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



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