邏輯之美(7)_快速排序

快速排序的高效性依賴於必定的運氣成分java

↑這麼講其實不嚴謹。準確來說,快速排序的高效性依賴於數學機率,且這裏的數學機率能夠保證——你的電腦在使用快速排序(正確實現的)給一組數據排序時,比插入排序或選擇排序要低效的機率比你的電腦此時被閃電擊中的機率還要低!算法

因其高效性,快速排序是當下應用最普遍的排序算法。數組

一種應用普遍的算法其效率竟然是靠機率來保證的,聽起來可能有點扯,究竟是如何?下面且仔細道來。dom

正文

相比於歸併排序,快速排序在保證高效的前提下並不須要那麼多的輔助空間(線性級別),這是它的一大優點。測試

快速排序的最基本算法思路究竟是怎樣?優化

快速排序的基本算法

與歸併排序相似,快速排序的算法也是一種分而治之的思路。以數組爲例,先將數組分紅兩個子數組,而後分別將兩個子數組排序(是不又想到了遞歸)。與歸併排序不一樣的是,歸併排序將兩個子數組排序後還需將兩個子數組歸併到一塊兒,已使數組總體有序。快速排序與之不一樣,快速排序中給兩個子數組排好序時,原始數組也就天然地總體有序了ui

特別須要注意的是,快速排序的實現依賴於一個很是重要的切分操做。就是以一個元素(的正確位置)爲基準,將原數組切分紅兩個待排序的子數組,子數組不包含這個切分位置(即不包含此位置上的元素,嚴格來說原數組被分紅了三個子數組!),左邊子數組的元素都不大於此元素的鍵值,右邊子數組的元素都不小於此元素的鍵值。spa

這意味着什麼?這意味着切分位置的元素已經呆在原數組總體有序時它該在的位置了!指針

因此說快速排序中給兩個子數組排好序時,原始數組也就天然地總體有序了。code

說這麼多,用圖片具象化展現下快速排序的過程:

快速排序基本過程,圖源維基百科

OK 捋完了基本邏輯思路,下面直接上代碼來看一種快速排序算法的經典實現。

快速排序的一種經典實現

基於遞歸的一種經典實現(Java 版本):

/** * <p>爲數組a 的 [start, end] 下標區間原地快速排序的遞歸實現</p> * @param a:待排序數組 * @param start,排序區間起始下標 * @param end,排序區間終止下標 */
    public static void sortQuick(int[] a, int start, int end){
        if (end <= start){
            return;
        }
        int j = clip(a, start, end);//切分操做完成後,數組 a 的 j 位置已放着總體有序時正確的元素!
        sortQuick_(a, start, j - 1);//將切分位置左邊的子數組排序
        sortQuick_(a, j + 1, end);//將切分位置右邊的子數組排序
        //數組達到總體有序
    }


/** * <p>快速排序的切分操做,將數組 a 切分爲 a[start, j - 1], a[j], [j + 1, end],返回 j </p> * @param a:待切數組 * @param start,起始下標 * @param end,終止下標 * @return j 切分點下標 */
    public static int clip (int[] a, int start, int end){
        int i = start, j = end + 1;//左右兩個掃描數組的指針
        int indexRandom = nextInt(start, end);//[start, end]區間裏的一個隨機位置
        exch(a, start, indexRandom);//將此隨機位置的元素交換到a[start]
        int clip = a[start];//切分元素,取[start, end]區間裏的一個隨機位置
        while (true){
            //掃描左右兩邊,並在須要時交換元素
            while (a[++i] < clip){//掃描左邊
                if (i == end){
                    break;
                }
            }
            while (a[--j] > clip){//掃描右邊
                if (j == start){
                    break;
                }
            }
            if (i >= j){//此條件成立則表示已總體掃描完
                break;
            }
            //i < j 時,交換兩個位置的元素
            exch(a, i, j);
        }
        exch(a, start, j);//將clip 放入正確位置 j
        //此時對於數組中的全部元素(鍵值),已達成 a[start, j - 1] <= a[j] <= a[j + 1, end]
        return j;
    }


/** * <p>返回 min <= 隨機數 <= max 的隨機整數數</p> * @param min:min * @param max:max * @return int i:指定閉區間內的隨機數 */
    public static int nextInt(int min, int max){
        return min + (int)(Math.random() * (max-min+1));
    }
複製代碼

以上代碼中最關鍵的是 clip 方法,最難理解的也是 clip 方法。

其實能夠這麼理解,每次進行的切分操做都能爲原數組排定一個元素(就是那個用來切分的元素),由於該元素左邊的元素(組成的子數組)都不大於它,而右邊的元素(組成的子數組)都不小於它,因此此切分元素確定已經在(原數組總體有序時)它該在的位置了。此時若是咱們把切分的左子數組和右子數組都接着排好序那麼原數組便達到了總體有序!clip 方法中的兩個指針(i 和 j)相遇時咱們將切分元素 a[start] 和當前左子數組最右邊一個元素(a[j])交換而後返回 j 便可。兩個指針 i 和 j 會在何時相遇呢?只會有兩種狀況:

i > j 或者 i == j

這點不難自行概括證實。

另外一種更簡潔的實現

其實咱們能夠在思惟層面更進一步,上面的實現咱們在 clip 操做裏實際上是把原數組分紅了三個子數組,左子數組,切分的中間元素(中間數組?),和右子數組。

必需要有這個中間元素嗎?我寫完上面的代碼後突然以爲沒有這個中間元素好像徹底沒問題,甚至能讓咱們的代碼更簡潔!

快速排序是一種分而治之的算法,上面咱們是把原數組分紅了三部分,左子數組,切分的中間元素(已在數組總體有序時它該在的位置),右子數組。原問題確實分紅了兩個更小的子問題(此時把左右數組排好序原數組就總體有序了),這就叫分而治之。從邏輯上來分析,沒有中間元素,就單純的把原數組分紅左右兩個子數組,只要右子數組裏的元素都不小於左子數組裏面的元素,把這兩個子數組排好序後原數組一樣能達到總體有序。這確實是一種更精簡的思路,直接來看下實現代碼:

/** * <p>爲數組a 的 [start, end] 下標區間原地快速排序的非遞歸實現</p> * @param a:待排序數組 * @param start,排序區間起始下標 * @param end,排序區間終止下標 */
    public static void sortQuick(int[] a, int start, int end){
        if (start >= end){
            return;
        }
        //遍歷數組的兩個指針,和用於切分數組的元素 clip,此方法將數組 a 切分紅兩個純粹的左右子數組,無多餘的中間元素
        int i = start, j = end, clip = a[nextInt(start, end)];
        while (i <= j){
            while (a[i] < clip){
                i++;
            }
            while (a[j] > clip){
                j--;
            }
            if (i < j){
                exch(a, i, j);
                i++;
                j--;
            }else if (i == j){
                i++;//或者j--
            }
        }
        /** * ↑捋一下邏輯,上面的循環走完後,j 剛剛比 i 大一 */
        sortQuick(a, start, j);//將左邊的子數組排序
        sortQuick(a, i, end);//將右邊的子數組排序
        //數組達到總體有序
    }

/** * <p>返回 min <= 隨機數 <= max 的隨機整數數</p> * @param min:min * @param max:max * @return int i:指定閉區間內的隨機數 */
    public static int nextInt(int min, int max){
        return min + (int)(Math.random() * (max-min+1));
    }
複製代碼

確實更簡潔了~

小結

快速排序的理想狀況是每次都恰好將數組對半切分,這樣算法運行起來最高效(成本最低)。想要每次都讓切分元素都恰好落在數組中間是很難作到的。快速排序實現的一大暗坑就是在切分不平衡時算法可能會極爲低效,好比第一次從數組中最小的元素開始切分,第二次從第二小的元素開始切分……這會致使一個大數組須要被切分太屢次。不過咱們上面實現的代碼能作到平均而言切分元素都在數組中間,咱們隨機選擇切分元素的操做就是爲使產生糟糕切分的可能性降到很低,盡力避免上述弊端。

總結

以上所述乃是最基本的快速排序,讀者還需好好消化吸取一下。快速排序的平均時間複雜度爲線性對數級別的 O(n log n),所需的空間複雜度根據具體實現的不一樣加以區別,如咱們上述的實現只需常數級別的輔助空間。特別注意對於很差的實現,快速排序最壞須要平方級別的時間複雜度。

上述分析其實不夠立體,對於一些典型用例,快速排序是要比咱們以前文章裏討論的排序算法都要快的,這點讀者不妨本身寫些測試用例實際跑跑對比看看其餘排序算法。

固然以上所述只是最基本的快速排序,其還有很大改進空間,例如針對含有大量重複元素數組優化的三向切分的快速排序算法。針對基本快速排序算法的改進暫不在本文討論範圍,之後有機會能夠單發篇文章好好聊聊此方面。

系列文章至此,主流幾種排序算法已所有講完,下篇聊啥呢?

相關文章
相關標籤/搜索