1. 程式人生 > >快速排序實現及其pivot的選取

快速排序實現及其pivot的選取

輸入 int 比較 運行時間 swa 計算 ddn 進行 實現

coursera上斯坦福的算法專項在講到快速排序時,稱其為最優雅的算法之一。快速排序確實是一種比較有效的排序算法,很多類庫中也都采用了這種排序算法,其最壞時間復雜度為$O(n^2)$,平均時間復雜度為$O(nlogn)$,且其不需要額外的存儲空間。

基本步驟

快速排序主要使用了分治的思想,通過選取一個pivot,將一個數組劃分為兩個子數組。其步驟為:
1.從數組中選擇一個元素作為pivot
2.重新排列數組,小於pivot的在pivot的左邊,大於pivot的在其右邊。
3.遞歸地對劃分後的左右兩部分重復上述步驟。

簡單的偽代碼如下:

技術分享圖片

其中最主要的就是partition劃分過程了。

劃分過程

partition過程需要首先選擇一個pivot,然後將小於pivot的元素放到左半部分,大於pivot的放到右半部分,並且最終pivot的位置及為其在排序好的數組中的最終位置。

這裏使用第一個元素作為pivot,若選擇其他元素作為pivot,則將其交換到第一個元素,這樣可以保證代碼的一致性及容易實現。示意圖如下:

技術分享圖片

這裏使用i和j,i和j最初為p+1的位置,在遍歷的過程中i始終指向>p的第一個元素,j始終指向當前待遍歷的元素,若a[j] < p,則將其與a[i]進行交換。相關過程如下:

技術分享圖片

基本實現如下:

    /**
    * a[l+1],...,a[i-1] < p
    * a[i],...,a[j-1] > p
    */
    private static int partition(int[] a, int l, int r) {
        int p = a[l];

        int i = l + 1;
        for (int j=l+1; j<=r; j++) {
            if (a[j] < p) {
                swap(a, j, i);
                i++;
            }
        }
        swap(a, l, i-1);
        return i-1;
    }

基本實現

public class QuickSort {
    public static void qSort(int[] a) {
        if (a == null || a.length <= 1) {
            return;
        }
    
        qSort(a, 0, a.length-1);
    }

    private static void qSort(int[] a, int l, int r)    {
        if (l >= r) {
            return;
        }

        int pos = partition(a, l, r);

        qSort(a, l, pos - 1);
        qSort(a, pos + 1, r);
    }

    /**
    * a[l+1],...,a[i-1] < p
    * a[i],...,a[j-1] > p
    */
    private static int partition(int[] a, int l, int r) {
        int p = a[l];
        int i = l + 1;
        for (int j=l+1; j<=r; j++) {
            if (a[j] < p) {
                swap(a, j, i);
                i++;
            }
        }
        swap(a, l, i-1);
        return i-1;
    }

    //返回pivot下標 選擇第一個元素
    private static int choosePivotFirst(int[] a, int l, int r) {
        return l;
    }
    
   private static void swap(int[] a, int x, int y) {
        int temp = a[x];
        a[x] = a[y];
        a[y] = temp;
    }

pivot的選取

根據斯坦福算法專項課,然我們實現三種不同的pivot選取方式,並計算相應比較次數,分別為choose first, choose last, median of three, 還可以進行隨機選取,這也是快速排序為什麽是一種隨機化算法。

pivot的選取決定了快速排序的運行時間,下面對幾種特殊情況進行分析:

1.最壞情況

假設我們始終選取第一個元素作為pivot, 並且輸入數組是有序的,那麽每次劃分後面所有元素都大於pivot, 每次只能將問題規模減少1,所以運行時間為$n+n-1+n-2+...+1$ = $O(n^2)$.

2.最好情況

最好情況為每次選取的pivot都能將數組平均地劃分為兩部分,由於劃分的過程為$O(n)$,所以總的運行時間為$$T(n) = 2T(n/2) + O(n)$$根據主方法,時間復雜度為O(nlogn)。

3.隨機選取

每次運行過程中,隨機選取pivot, 通常能得到比較好的結果。

選取方式及實現

斯坦福算法專項課上讓我們實現三種不同的選取方式,選取第一個,最後一個,以及三數取中。

1.choose first

該種方式最為簡單,只需返回子數組的第一個元素下標即可,下面為其實現:

//返回pivot下標 選擇第一個元素
private static int choosePivotFirst(int[] a, int l, int r) {
    return l;
}

2.choose last

選擇最後一個元素,實現如下:

//選擇最後一個元素作為pivot
private static int choosePivotLast(int[] a, int l, int r) {
        return r;
}

3.median-of-three

選取第一個、最後一個以及中間的元素的中位數,如4 5 6 7, 第一個4, 最後一個7, 中間的為5, 這三個數的中位數為5, 所以選擇5作為pivot,8 2 5 4 7, 三個元素分別為8 5 7, 中位數為7, 所以選擇最後一個元素7作為pivot,其實現如下:

//median-of-three pivot rule
private static int choosePivotMedianOfThree(int[] a, int l, int r) {    
    int mid = 0;
    if ((r-l+1) % 2 == 0) {
        mid = l + (r-l+1)/2 - 1;
    } else {
        mid = l + (r-l+1)/2;
    }

    //只需要找出中位數即可,不需要交換
    //有的版本也可以進行交換
    if (((a[l]-a[mid]) * (a[l]-a[r])) <= 0) {
        return l;
    } else if (((a[mid]-a[l]) * (a[mid]-a[r])) <= 0)    {
        return mid;
    } else {
        return r;
    }
}

最後的劃分過程如下:

private static int partition(int[] a, int l, int r) {
    //pivot選擇方式
    //int pi = choosePivotFirst(a, l, r);
    //int pi = choosePivotLast(a, l, r);
    int pi = choosePivotMedianOfThree(a, l, r);

    //始終將第一個元素作為pivot, 若不是, 則與之交換
    if (pi != l) {
        swap(a, pi, l);
    }
    int p = a[l];

    int i = l + 1;
    for (int j=l+1; j<=r; j++) {
        if (a[j] < p) {
            swap(a, j, i);
            i++;
        }
    }
    swap(a, l, i-1);
    return i-1;
}

註意最後的劃分過程相比於之前增加的pivot的選取方式,而不是單純地將第一個元素作為pivot, 可以看到,若第一個元素不是pivot, 需要將pivot與第一個元素進行交換,這樣保證代碼的統一性。

總結與感想

1.學會體會這些算法背後的思想,為什麽要這樣設計

2.對於比較復雜的算法,學會使用特殊情況進行分析

參考資料:

(1) coursera斯坦福算法專項課part1

(2) 維基百科快速排序

快速排序實現及其pivot的選取