圖解快速排序及雙路三路快速排序

前言

以前咱們介紹了交換類排序中的冒泡排序,此次咱們介紹另外一種交換類排序叫作快速排序。快速排序的優勢是原地排序,不佔用額外空間,時間複雜度是O(nlogn)java

固然,對於快速排序來講,它也是有缺點的,它對於含有大量重複元素的數組排序效率是很是低的,時間複雜度會降爲O(n^2)。此時須要使用改進的快速排序—雙路快速排序,在雙路快速排序的基礎上,咱們又進一步優化獲得了三路快速排序。數組

快速排序

快速排序的基本思想是:經過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的全部數據都比另一部分的全部數據都要小,而後再按此方法對這兩部分數據分別進行快速排序,整個排序過程能夠遞歸進行,以此達到整個數據變成有序序列。

快速排序的步驟以下:dom

  1. 把第一個元素做爲分界的標定點,用l指向它。

  2. 遍歷右邊元素,在遍歷的過程當中,咱們整理數組,一部分小於v,一部分大於v,用j指向小於v和大於v的分界點,用i指向當前訪問的元素e,此時,數組arr[l+1...j]<varr[j+1...i-1]>v

  3. e>v,那麼直接將e合併在大於v那麼部分的後面,而後i++繼續比較後面的元素。

  4. e<v,那麼將e移動到j所指向元素的後一個元素,接着j++,而後i++繼續比較後面的元素。

  5. 使用這種方式對整個數組進行一次遍歷,遍歷完後數組被分紅三部分,左邊部分是v,中間部分是>v,右邊部分是<v

  6. 最後,咱們讓l指向的元素和j指向的元素交換,這樣就v這個元素進行了快速排序,v左邊元素都小於v,右邊元素都大於v

如今咱們使用上述方法對數組[2, 1, 4, 3, 7, 8, 5, 6]進行快速排序,下圖展現了整個快速排序的過程:性能

快速排序代碼:優化

public static void sort(Comparable[] arr) {
    int n = arr.length;
    sort(arr, 0, n - 1);
}

// 遞歸使用快速排序,對arr[l...r]的範圍進行排序
private static void sort(Comparable[] arr, int l, int r) {
    if (l >= r) {
        return;
    }
    // 對arr[l...r]部分進行partition操做, 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
    int p = partition(arr, l, r);
    sort(arr, l, p - 1);
    sort(arr, p + 1, r);
}

private static int partition(Comparable[] arr, int l, int r) {
    // 最左元素做爲標定點
    Comparable v = arr[l];
    int j = l;
    for (int i = l + 1; i <= r; i++) {
        if (arr[i].compareTo(v) < 0) {
            swap(arr, j + 1, i);
            j++;
        }
    }
    swap(arr, l, j);
    return j;
}

優化的快速排序

通過上述介紹,咱們能夠發現快速排序不能保證每次切分的子數組大小相等,所以就可能一邊很小,一邊很大。對於一個有序數組,快速排序的時間複雜度就變成了O(n^2),至關於樹退化成了鏈表,下圖展現了這種變化:spa

上述咱們是固定使用左邊的第一個元素做爲標定元素,如今咱們隨機挑選一個元素做爲標定元素。此時咱們第一次選中第一個元素的機率爲 1/n,第二次又選中第二個元素 1/n-1,以此類推,發生以前退化成鏈表的機率爲1/n(n-1)(n-2)....,當 n 很大時,這種機率幾乎爲 0。3d

另外一個優化就是對小規模數組使用插入排序,由於遞歸會使得小規模問題中方法的調用過於頻繁,而插入排序對小規模數組排序是很是快的。code

優化的快速排序代碼:blog

public static void sort(Comparable[] arr) {
    int n = arr.length;
    sort(arr, 0, n - 1);
}

// 遞歸使用快速排序,對arr[l...r]的範圍進行排序
private static void sort(Comparable[] arr, int l, int r) {
    // 對於小規模數組, 使用插入排序
    if (r - l <= 15) {
        InsertionSort.sort(arr, l, r);
        return;
    }
    // 對arr[l...r]部分進行partition操做, 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
    int p = partition(arr, l, r);
    sort(arr, l, p - 1);
    sort(arr, p + 1, r);
}

private static int partition(Comparable[] arr, int l, int r) {
    // 隨機在arr[l...r]的範圍中, 選擇一個數值做爲標定點pivot
    swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
    Comparable v = arr[l];
    int j = l;
    for (int i = l + 1; i <= r; i++) {
        if (arr[i].compareTo(v) < 0) {
            swap(arr, j + 1, i);
            j++;
        }
    }
    swap(arr, l, j);
    return j;
}

雙路快速排序

對於含有大量重複元素的數組,使用上述的快速排序效率是很是低的,由於在咱們上面的判斷中,若是元素小於v,則將元素放在<v部分,若是元素大於等於v,則放在>v部分。此時,若是數組中有大量重複元素,>v部分會變得很長,致使左右兩邊不均衡,性能下降。排序

雙路快速排序的步驟以下:

  1. <v>v兩部分放在數組的兩端,用i指向<v部分的下一個元素,用j指向>v部分的前一個元素。

  2. i開始向後遍歷,若是遍歷的元素e<v,則繼續向後遍歷,直到遍歷的元素e>=v,則中止遍歷。一樣從j開始向前遍歷,若是遍歷的元素e>v,則繼續向前遍歷,直到遍歷的元素e<=v,則中止遍歷。

  3. 交換i指向的元素和j指向的元素。而後i++j--繼續比較下一個。

雙路快速排序的代碼:

public static void sort(Comparable[] arr) {
    int n = arr.length;
    sort(arr, 0, n - 1);
}

private static void sort(Comparable[] arr, int l, int r) {
    // 對於小規模數組, 使用插入排序
    if (r - l <= 15) {
        InsertionSort.sort(arr, l, r);
        return;
    }
    int p = partition(arr, l, r);
    sort(arr, l, p - 1);
    sort(arr, p + 1, r);
}

private static int partition(Comparable[] arr, int l, int r) {

    // 隨機在arr[l...r]的範圍中, 選擇一個數值做爲標定點pivot
    swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
    Comparable v = arr[l];
    int i = l + 1, j = r;
    while (true) {
        // 注意這裏的邊界, arr[i].compareTo(v) < 0, 不能是arr[i].compareTo(v) <= 0
        // 不加等號若是遇到相等的狀況,這時候while循環就會退出,即交換i和j的值,使得對於包含大量相同元素的數組, 雙方相等的數據就會交換,這樣就能夠必定程度保證兩路的數據量平衡

        // 從i開始向後遍歷,若是遍歷的元素e<v,則繼續向後遍歷,直到遍歷的元素e>=v,則中止遍歷
        while (i <= r && arr[i].compareTo(v) < 0) {
            i++;
        }
        // 從j開始向前遍歷,若是遍歷的元素e>v,則繼續向前遍歷,直到遍歷的元素e<=v,則中止遍歷
        while (j >= l + 1 && arr[j].compareTo(v) > 0) {
            j--;
        }
        if (i >= j) {
            break;
        }
        swap(arr, i, j);
        i++;
        j--;
    }
    // 此時j指向的元素是數組中最後一個小於v的元素, i指向的元素是數組中第一個大於v的元素
    swap(arr, l, j);
    return j;
}

三路快速排序

三路快速排序的步驟以下:

  1. 在雙路快速排序的基礎上,咱們把等於v的元素單獨做爲一個部分。lt指向小於v部分的最後一個元素,gt指向大於v部分的第一個元素。

  2. i開始向後遍歷,若是遍歷的元素e=v,則e直接合併到=v部分,而後i++繼續遍歷。若是遍歷的元素e<v,則將e=v部分的第一個元素(lt+1指向的元素)交換,而後lt++i++繼續遍歷。若是遍歷的元素e>v,則將e>v部分前一個元素(gt-1指向的元素)交換,而後gt--,不過此時i不須要改變,由於i位置的元素是和gt位置前面的空白元素交換過來的。
  3. 遍歷完後i=gt,而後將l指向元素和lt指向元素交換。

  4. <v部分和>v部分進行以上操做。

三路快速排序相比雙路快速排序的優點在於:減小了對重複元素的比較操做,由於重複元素在一次排序中就已經做爲單獨一部分排好了,以後只須要對不等於該重複元素的其餘元素進行排序。

三路快速排序代碼:

public static void sort(Comparable[] arr) {
    int n = arr.length;
    sort(arr, 0, n - 1);
}

private static void sort(Comparable[] arr, int l, int r) {
    
    // 對於小規模數組, 使用插入排序
    if (r - l <= 15) {
        InsertionSort.sort(arr, l, r);
        return;
    }
    // 隨機在arr[l...r]的範圍中, 選擇一個數值做爲標定點pivot
    swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
    Comparable v = arr[l];
    int lt = l;     // arr[l+1...lt] < v
    int gt = r + 1; // arr[gt...r] > v
    int i = l + 1;    // arr[lt+1...i) == v
    while (i < gt) {
        if (arr[i].compareTo(v) < 0) {
            swap(arr, i, lt + 1);
            i++;
            lt++;
        } else if (arr[i].compareTo(v) > 0) {
            swap(arr, i, gt - 1);
            gt--;
        } else { // arr[i] == v
            i++;
        }
    }
    swap(arr, l, lt);

    sort(arr, l, lt - 1);
    sort(arr, gt, r);
}

總結

本文介紹了快速排序、快速排序的優化、雙路快速排序和三路快速排序。

對於快速排序,咱們須要選擇合適的標定點,使得標定點的兩邊平衡;在快速排序中遞歸到小數組時,咱們可使用插入排序替換遞歸,減小沒必要要的開銷。

對於雙路快速排序和三路快速排序,咱們使用的場合是數組中存在大量重複元素。

最後,提示一下 JDK 底層的排序使用的就是插入排序 + 雙路快速排序 + 歸併排序的組合

相關文章
相關標籤/搜索