Sorting 排序演算法: Quick Sort 快速排序

超悠閒發表於2020-10-29

Sorting 排序演算法: Quick Sort 快速排序

簡介

快速排序作為最常被應用的排序演算法,因為其擁有最好的平均時間效率,同時只用了常數的額外空間,所以受到大多數應用的青睞。

參考

正文

演算法思想原理

輸入

/* 傳入引數 */
int[] A // 原陣列(可以是任何型別的資料,這邊使用 int 型別為示例)

/* 限制 */
int n // 陣列長度

演算法思想

快速排序也和歸併排序一樣採用了分治的思想,但是與歸併不同的是劃分(partition)階段並不只是單純的分成兩半,而是選定一個基準值(pivot),將整個陣列劃分成"比 pivot 小"和"比 pivot 大"的兩半,然後再分別排序。分類時的交換使得合併(merge)階段只需要花費常數的時間代價即可完成

  • 動圖

演算法流程

虛擬碼(參考:演算法導論)

Quick-Sort(A)
    Quick-Sort(A, 0, A.length)

Quick-Sort(A, l, r)
    if l < r
        q = partition(A, l, r)
        Quick-Sort(A, l, q - 1)
        Quick-Sort(A, q + 1, r)

Partition(A, l, r)
    pivot = A[r]
    i = l - 1
    for j = l to r - 1
        if A[j] <= pivot
            i = i + 1
            exchange A[i] and A[j]
    exchange A[i + 1] and A[r]
    return i + 1

與歸併排序相同,分治演算法使用左右界作為引數,以 [0 … A.length - 1] 區間為起始。當子陣列長度大於 1 時(l < r),就進行劃分,然後再分別對兩個子陣列進行排序。
在劃分階段,直接選取最右邊的數作為基準(pivot),i 指向左子陣列(比 pivot 小)的末尾,j 遍歷一遍陣列,遇到比 pivot 小的數就交換到 i 位置。迴圈結束時將第 i + 1 個數也就是第一個大於 pivot 的值與 pivot 交換,就相當於劃分出以 i + 1 位置為界:i + 1 以左的都小於(等於) pivot,以右的都大於 pivot。如此遞迴知道陣列長度為 1。

演算法複雜度分析

先說結論:

  1. 時間複雜度:最壞 O(n2) / 平均 O(nlogn)
  2. 空間複雜度:O(1)
  3. 原址排序
  4. 不穩定的
  • 時間複雜度:最壞 O(n2) / 平均 O(nlogn)

在最壞的情況下每次劃分都將所有元素劃分到同一邊,這時時間複雜度就接近 O(n2),然而這種情況是極少出現的,只要中間有幾次有分出兩個子陣列,漸進時間上還是趨近於 O(nlogn) 的,所以平均效率上是極好的

  • 空間複雜度:1

在 partition 階段進行兩兩交換,沒有使用其他額外的空間,複雜度為 O(1)

  • 原址排序

在原陣列內交換元素,使用區間作為排序界線

  • 不穩定的

由於交換是小於等於 pivot 的一律向前交換,所以當 pivot 選取到存在重複的元素時會出現相對順序改變的問題,所以演算法是不穩定的

Java 實現

  • QuickSort.java
public class QuickSort {
    public static void sort(int[] A) {
        sort(A, 0, A.length - 1);
    }

    private static void sort(int[] A, int l, int r) {
        if (l < r) {
            int m = partition(A, l, r);
            sort(A, l, m - 1);
            sort(A, m + 1, r);
        }
    }

    private static int partition(int[] A, int l, int r) {
        int pivot = A[r];
        int i = l - 1;
        for (int j = l; j < r; j++) {
            if(A[j] <= pivot) {
                int tmp = A[++i];
                A[i] = A[j];
                A[j] = tmp;
            }
        }
        A[r] = A[i + 1];
        A[i + 1] = pivot;
        return i + 1;
    }
}

  • QuickSortTest.java
public class QuickSortTest {
    @Test
    public void test() {
        int[] A = new int[]{1,3,5,7,9,2,4,6,8,0};
        int[] ans = new int[]{0,1,2,3,4,5,6,7,8,9};
        System.out.println("origin: " + Arrays.toString(A));
        QuickSort.sort(A);
        System.out.println("sorted: " + Arrays.toString(A));
        assertArrayEquals(ans, A);
    }
}
  • 輸出
origin: [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
sorted: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

結語

作為擁有最好的比較排序代價,同時使用最少的額外空間(O(1))的比較排序演算法,快速排序是最常被呼叫的排序演算法之一。然而在基本款的快排之上,我們還可以在 pivot 的選取上進行優化,使得 partition 過後的子陣列更加平均以提升快速排序的平均效率。

相關文章