排序算法06:快速排序

算法介紹

  快速排序是一種分治的排序算法。排序邏輯爲:先挑一個元素來切分數組,最終讓該元素的左側都小於該元素,右側的所有元素都大於該元素。遞歸的讓左側和右側分別執行該操作,最終讓整個數組變得有序。

快速排序示意圖:

  咋眼一看快速排序跟歸併排序很像,其實區別挺明顯的。歸併排序將數組分成兩個子數組分別排序,並將有序的子數組歸併以將整個數組排序;而快速排序將數組排序的方式則是當左右兩個數組有序時整個數組爲自然有序狀態。

快速排序的切分

  快速排序的核心在於數組的切分。切分的邏輯爲,維護兩個指針i和j,i從前往後移動,j從後往前移動。最終讓切分元素的左側都小於切分元素,右側都大於切分元素。

切分示意圖如下:

Javascript代碼如下:

/**
     * 將數組a切分爲a[lo...i-1],a[i],a[i+1,hi]
     * @param a     需要切分的數組
     * @param lo    起始位置
     * @param hi    結束位置
     */
    function partition(a,lo,hi) {
        var i = lo,j=hi+1;      //- i,j爲左右掃描的指針
        var v = a[lo];          //- 選擇第lo元素爲切分元素,讓lo左邊的都小於a[lo],右邊的都大於a[lo]
        while(true){
            //不斷從前向後移動指針
            while (a[++i] < v){
                if(i==hi){break;}
            }
            //不斷的從後往前移動指針
            while (v < a[--j]){
                if(j==lo){break;}
            }
            if(i>=j){break;}
            //較小的往前放,較小的往後放
            exch(a,i,j);
        }
        //把切分的元素放到正確的位置,交換lo與j的位置
        exch(a,lo,j);
        return j;
    }

一次切分的切分軌跡如下:

排序實現

切分完成了排序就比較簡單了,代碼如下:

    /**
     * 快速排序
     * @param a     需要排序的數組
     * @param lo    起始位置
     * @param hi    結束位置
     */
    function sort(a,lo,hi) {
        if(hi<=lo)return;
        var j = partition(a,lo,hi); //- 切分數組,讓a[j]左邊的都小於它,右邊的都大於它。
        sort(a,lo,j-1);             //- 將左半部分a[lo..j-1]排序
        sort(a,j+1,hi);             //- 將右半部份a[j+1...hi]排序
    }

完整代碼

Javascript實現


/**
 * Created by YiYing on 2017/4/30.
 */

(function (W) {
    /**
     * 快速排序
     * @param a     需要排序的數組
     * @param lo    起始位置
     * @param hi    結束位置
     */
    function sort(a,lo,hi) {
        if(hi<=lo)return;
        var j = partition(a,lo,hi); //- 切分數組,讓a[j]左邊的都小於它,右邊的都大於它。
        sort(a,lo,j-1);             //- 將左半部分a[lo..j-1]排序
        sort(a,j+1,hi);             //- 將右半部份a[j+1...hi]排序
    }

    /**
     * 將數組a切分爲a[lo...i-1],a[i],a[i+1,hi]
     * @param a     需要切分的數組
     * @param lo    起始位置
     * @param hi    結束位置
     */
    function partition(a,lo,hi) {
        var i = lo,j=hi+1;      //- i,j爲左右掃描的指針
        var v = a[lo];          //- 選擇第lo元素爲切分元素,讓lo左邊的都小於a[lo],右邊的都大於a[lo]
        while(true){
            //不斷從前向後移動指針
            while (a[++i] < v){
                if(i==hi){break;}
            }
            //不斷的從後往前移動指針
            while (v < a[--j]){
                if(j==lo){break;}
            }
            if(i>=j){break;}
            //較小的往前放,較小的往後放
            exch(a,i,j);
        }
        //把切分的元素放到正確的位置,交換lo與j的位置
        exch(a,lo,j);
        return j;
    }

    /**
     * 交換數組中m與n的位置
     * @param a  數組
     * @param m
     * @param n
     */
    function exch(a,m,n) {
        var swap    = a[m];
        a[m] = a[n];
        a[n] = swap;
    }

    W.QuickSort = function (a) {
        sort(a,0,a.length);
    }
})(window);

//測試代碼
(function () {
    //var arr = [4,5,6,0,3,5,21,7,9,0,1];
    //console.log(arr);
    var chars = ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'];
    var arr = [];
    for(var i=0;i<1000000;i++){
        var id = Math.ceil(Math.random()*35);
        arr[i] = chars[id];
    }
    console.time("QuickSort");
    QuickSort(arr);      //- chrome下一百萬數據平均250ms,如果不計算長度,則小於200ms
    console.timeEnd("QuickSort");
})();

Java實現


package com.algs;


public class Quick {

    public static void sort(Comparable[] a) {
        sort(a, 0, a.length - 1);
    }

    //讓數組從a[lo] 到 a[hi]有序
    private static void sort(Comparable[] a, int lo, int hi) {
        if (hi <= lo) return;
        int j = partition(a, lo, hi);
        sort(a, lo, j-1);
        sort(a, j+1, hi);
    }

    // 切分數組a[lo..hi] 最終讓數組滿足如下狀態: a[lo..j-1] <= a[j] <= a[j+1..hi]
    // 並返回j
    private static int partition(Comparable[] a, int lo, int hi) {
        int i = lo;
        int j = hi + 1;
        Comparable v = a[lo];
        while (true) {
            while (less(a[++i], v))
                if (i == hi) break;
            while (less(v, a[--j]))
                if (j == lo) break;
            if (i >= j) break;
            exch(a, i, j);
        }
        // 把切分的元素放到j的位置上
        exch(a, lo, j);
        return j;
    }

     // 判斷v < w ?
     private static boolean less(Comparable v, Comparable w) {
         return v.compareTo(w) < 0;
     }

     // 交換 a[i]和 a[j]
     private static void exch(Object[] a, int i, int j) {
         Object swap = a[i];
         a[i] = a[j];
         a[j] = swap;
     }

     public static void show(Integer[] a){
        for (int i = 0; i < a.length; i++) {
             System.out.print(a[i]+" ");
         }
    }

    public static void main(String[] args) {
        Integer[] a = {1,55,32,2,3,4};
        sort(a);
        show(a);
    }

}

總結

  1. 不穩定。
  2. 原地排序。
  3. 時間複雜度爲NlogN
  4. 空間複雜度爲lgN
  5. 快速排序最好的情況是每次都正好能將數組對半分
  6. 如果使用左右兩個輔助數組可以方便實現切分,但把數組複製回去的成本會大大降低排序的速度
  7. 歸併排序和希爾排序一般都比快速排序慢。原因爲:快速排序切分中用一個遞增的索引將數組元素和一個定值比較。而歸併排序和希爾排序在內循環中移動數據的頻率較高。

GitHub:https://github.com/AlbertKnag/algs-practice

上一篇:排序算法05:歸併排序
下一篇:排序算法07:三向快速排序

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章