數據結構和算法面試題系列—排序算法之快速排序

這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏git

0 概述

快速排序也是基於分治模式,相似歸併排序那樣,不一樣的是快速排序劃分最後不須要merge。對一個數組 A[p..r] 進行快速排序分爲三個步驟:github

  • 劃分: 數組 A[p...r] 被劃分爲兩個子數組 A[p...q-1]A[q+1...r],使得 A[p...q-1] 中每一個元素都小於等於 A[q],而 A[q+1...r] 每一個元素都大於 A[q]。劃分流程見下圖。
  • 解決: 經過遞歸調用快速排序,對子數組分別排序便可。
  • 合併:由於兩個子數組都已經排好序了,且已經有大小關係了,不須要作任何操做。

快速排序劃分

快速排序算法不算複雜的算法,可是實際寫代碼的時候倒是最容易出錯的代碼,寫的不對就容易死循環或者劃分錯誤,本文代碼見 這裏面試

1 樸素的快速排序

這個樸素的快速排序有個缺陷就是在一些極端狀況如全部元素都相等時(或者元素自己有序,如 a[] = {1,2,3,4,5}等),樸素的快速算法時間複雜度爲 O(N^2),而若是可以平衡劃分數組則時間複雜度爲 O(NlgN)算法

/**
 * 快速排序-樸素版本
 */
void quickSort(int a[], int l, int u)
{
    if (l >= u) return;

    int q = partition(a, l, u);
    quickSort(a, l, q-1);
    quickSort(a, q+1, u);
}

/**
 * 快速排序-劃分函數
 */
int partition(int a[], int l, int u)
{
    int i, q=l;
    for (i = l+1; i <= u; i++) {
        if (a[i] < a[l])
            swapInt(a, i, ++q);
    }
    swapInt(a, l, q);
    return q;
}
複製代碼

2 改進-雙向劃分的快速排序

一種改進方法就是採用雙向劃分,使用兩個變量 iji 從左往右掃描,移太小元素,遇到大元素中止;j 從右往左掃描,移過大元素,遇到小元素中止。而後測試i和j是否交叉,若是交叉則中止,不然交換 ij 對應的元素值。數組

注意,若是數組中有相同的元素,則遇到相同的元素時,咱們中止掃描,並交換 ij 的元素值。雖然這樣交換次數增長了,可是卻將全部元素相同的最壞狀況由 O(N^2) 變成了差很少 O(NlgN) 的狀況。好比數組 A={2,2,2,2,2}, 則使用樸素快速排序方法,每次都是劃分 n 個元素爲 1 個和 n-1 個,時間複雜度爲 O(N^2),而使用雙向劃分後,第一次劃分的位置是 2,基本能夠平衡劃分兩部分。代碼以下:bash

/**
 * 快速排序-雙向劃分函數
 */
int partitionLR(int a[], int l, int u, int pivot)
{
    int i = l;
    int j = u+1;
    while (1) {
        do {
            i++;
        } while (a[i] < pivot && i <= u); //注意i<=u這個判斷條件,不能越界。

        do {
            j--;
        } while (a[j] > pivot);

        if (i > j) break;

        swapInt(a, i, j);
    }

    // 注意這裏是交換l和j,而不是l和i,由於i與j交叉後,a[i...u]都大於等於樞紐元t,
    // 而樞紐元又在最左邊,因此不能與i交換。只能與j交換。
    swapInt(a, l, j);

    return j;
}

/**
 * 快速排序-雙向劃分法
 */
void quickSortLR(int a[], int l, int u)
{
    if (l >= u) return;

    int pivot = a[l];
    int q = partitionLR(a, l, u, pivot);
    quickSortLR(a, l, q-1);
    quickSortLR(a, q+1, u);
}
複製代碼

雖然雙向劃分解決了全部元素相同的問題,可是對於一個已經排好序的數組仍是會達到 O(N^2) 的複雜度。此外,雙向劃分還要注意的一點是代碼中循環的寫法,若是寫成 while(a[i]<t) {i++;} 等形式,則當左右劃分的兩個值都等於樞紐元時,會致使死循環。數據結構

3 繼續改進—隨機法和三數取中法取樞紐元

爲了解決上述問題,能夠進一步改進,經過隨機選取樞紐元或三數取中方式來獲取樞紐元,而後進行雙向劃分。三數取中指的就是從數組A[l... u]中選擇左中右三個值進行排序,並使用中值做爲樞紐元。如數組 A[] = {1, 3, 5, 2, 4},則咱們對 A[0]、A[2]、A[4] 進行排序,選擇中值 A[4](元素4) 做爲樞紐元,並將其交換到 a[l] ,最後數組變成 A[] = {4 3 5 2 1},而後跟以前同樣雙向排序便可。dom

/**
 * 隨機選擇樞紐元
 */
int pivotRandom(int a[], int l, int u)
{
    int rand = randInt(l, u);
    swapInt(a, l, rand); // 交換樞紐元到位置l
    return a[l];
}

/**
 * 三數取中選擇樞紐元
 */
int pivotMedian3(int a[], int l, int u)
{
     int m = l + (u-l)/2;

     /*
      * 三數排序
      */
     if( a[l] > a[m] )
        swapInt(a, l, m);

     if( a[l] > a[u] )
        swapInt(a, l, u);

     if( a[m] > a[u] )
        swapInt(a, m, u);

     /* assert: a[l] <= a[m] <= a[u] */
     swapInt(a, m, l); // 交換樞紐元到位置l

     return a[l];
}
複製代碼

此外,在數據基本有序的狀況下,使用插入排序能夠獲得很好的性能,並且在排序很小的子數組時,插入排序比快速排序更快,能夠在數組比較小時選用插入排序,而大數組才用快速排序。數據結構和算法

4 非遞歸寫快速排序

非遞歸寫快速排序着實比較少見,不過練練手老是好的。須要用到棧,注意壓棧的順序。代碼以下:函數

/**
 * 快速排序-非遞歸版本
 */
void quickSortIter(int a[], int n)
{
    Stack *stack = stackNew(n);
    int l = 0, u = n-1;
    int p = partition(a, l, u);

    if (p-1 > l) { //左半部分兩個邊界值入棧
        push(stack, p-1); 
        push(stack, l);
    }

    if (p+1 < u) { //右半部分兩個邊界值入棧
        push(stack, u);
        push(stack, p+1);
    }

    while (!IS_EMPTY(stack)) { //棧不爲空,則循環劃分過程
        l = pop(stack);
        u = pop(stack);
        p = partition(a, l, u);

        if (p-1 > l) {
            push(stack, p-1);
            push(stack, l);
        }

        if (p+1 < u) {
            push(stack, u);
            push(stack, p+1);
        }
    }
}
複製代碼

參考資料

  • 《數據結構和算法-C語言實現》
  • 《算法導論》
相關文章
相關標籤/搜索