快速排序實現及pivot的選取

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

基本步驟

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

簡單的偽代碼如下:

<img src="http://ou5lyiz64.bkt.clouddn.com/img/qsort%E9%80%89%E5%8C%BA_001.png" style="zoom: 50%" />

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

劃分過程

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

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

<img src="http://ou5lyiz64.bkt.clouddn.com/img/qsort%E9%80%89%E5%8C%BA_002.png" style="zoom:50%" />

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

<img src="http://ou5lyiz64.bkt.clouddn.com/img/qsort%E9%80%89%E5%8C%BA_003.png" style="zoom:50%" />

基本實現如下:

    /**
    * 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, 每次只能將問題規(guī)模減少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與第一個元素進行交換,這樣保證代碼的統(tǒng)一性。

總結與感想

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

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

參考資料:

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

(2) 維基百科快速排序

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容