邏輯之美(4)_希爾排序

希爾排序是一種改進後的,更高效的插入排序java

開篇

本文最好結合上篇插入排序閱讀,由於希爾排序實際上是插入排序改進而來的一種更高效的插入排序。此排序算法由 Donald Shell 於 1959 年提出,故得此名。算法

希爾排序是比普通插入排序要更高效一些的。從最壞時間複雜度來講,插入排序的最壞時間複雜度是平方級別的 О(n²),而希爾排序的最壞時間複雜度爲稍差於線性對數級別的 О(n log²n) ,好像有點繞,其實這等價於 О(n (log n)²) 。數組

希爾排序是如何改進插入排序的?答案是步長。什麼意思呢?插入排序中元素值的比較和移動是按步長爲1一個一個來的,希爾排序的改進思路是這樣子,咱們一開始先不以步長爲1來操做數組中的元素。設如今數組的長度爲 n,Donald Shell 當年建議咱們一開始以 n/2 爲步長對數組分組進行插入排序,而後再將步長除以 2 再對數組分組進行插入排序,步長最後總會迭代爲 1。當步長變爲 1 時,整個算法其實回到了最原始的插入排序(此步長序列一般不是效率最高的,這個咱們後面會說到)。你會發現,希爾排序實際上是遞歸地將數組分組反覆對其進行插入排序,很有點分而治之的意思。千萬注意這裏的按步長分組,這是理解希爾排序的關鍵所在,下面我偷懶直接把維基百科上的一個例子搬過來加深下理解。post

設有這樣一隻整型數組: [13, 14, 94, 33, 82, 25, 59, 94, 65, 23, 45, 27, 73, 25, 39, 10],若是咱們以步長序列(5, 3, 1)對其進行希爾排序。剛開始,咱們能夠經過將這組數字放在有5列(也就是把數組中的數分紅五組)的表中來更好地描述算法,這樣數組元素看起來是這樣的:測試

//表1,步長爲 5 對數組元素分組。注意每列是一組,不是行!不是行!不是行!
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
複製代碼

這裏千萬注意是怎麼對數組中的元素分組的,以步長爲 5 對數組元素分組,不是連續的五個數爲一組,而是:spa

[13, 14, 94, 33, 82, 25, 59, 94, 65, 23, 45, 27, 73, 25, 39, 10] -> 原數組
-13------------------25 -----------------45------------------10  -> [13, 25, 45, 10],這是個步長爲 5 的子數組
-----14------------------59 -----------------27----------------  -> [14, 59, 27],    又是個步長爲 5 的子數組
---------94------------------94 -----------------73------------  -> [94, 94, 73],    又是個步長爲 5 的子數組
-------------33------------------65 -----------------25--------  -> [33, 65, 25],    又是個步長爲 5 的子數組
-----------------82------------------23 -----------------39----  -> [82, 23, 39],    又是個步長爲 5 的子數組
複製代碼

這樣給數組中元素分組的。code

以步長爲 5 對原數組進行插入排序,也就是對錶 1 中每一列組成的子數組分別進行插入排序,待每列都排好序後數組變成:排序

10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
複製代碼

上述四行數字,再依序接在一塊兒時咱們獲得:[10, 14, 73, 25, 23, 13, 27, 94, 33, 39, 25, 59, 94, 65, 82, 45],可發現這時10 已經移至數組總體有序時的正確位置了,而後再以3爲步長進行分組:遞歸

10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
複製代碼

排序後數組變成:隊列

10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
複製代碼

最後以1爲步長進行排序,此時就是簡單原始的插入排序了。

正文

OK 但願上面說那麼多能讓你徹底理解希爾排序是怎麼一回事。下面咱們直接上代碼,代碼邏輯徹底遵守上面所梳理邏輯而寫,可能會顯得有點囉嗦,但卻易於理解(步長序列咱們使用數組長度除以 2 迭代至 1):

/** * @see: 希爾排序的 Java 實現 * @param array: 待排序數組,咱們採用原地排序 */
    public static void sortShell(int[] array){
        //步長迭代,步長越大,分的子數組越多。步長爲1時只有一個子數組,就是原數組自己,步長最小爲 1
        for (int step = array.length/2; step >= 1; step /= 2){
            //從第一個元素始,按步長給數組元素分組分組,對每組進行插入排序
            for (int start = 0; start < step; start++){
                //對當前步長分出來的其中一個子數組單獨作插入排序
                sortInsert(array, start, step);
            }
        }
    }

/** * @see: 插入排序的 Java 實現,只對數組中按步長分的子數組進行插入排序 * @param array: 待排序數組,咱們採用原地排序 * @param start: 排序從數組的哪一個元素開始 * @param step: 遍歷步長 */
    public static void sortInsert(int[] array, int start, int step){
        // 開始對子數組進行插入排序排序,千萬注意子數組是按步長分出來的,不是連續地分出來的
        // 從子數組第二個元素開始遍歷,固然子數組長度可能小於2
        for (int slow = start + step; slow < array.length; slow += step){
            //待從新插入元素 array[slow]
            int insertion = array[slow];
            //內循環遍歷,主要爲肯定待插入元素array[slow]的待插入位置
            int fast = slow - step;
            for (; fast >= 0; fast -= step){
                if (array[fast] > insertion){
                    array[fast + step] = array[fast];
                }else {
                    //待插入元素的待插入位置,老是從後往前看,最後一個值比它大的那個位置,
                    // 值比它大的那些值總體日後移動一個位置
                    break;
                }
            }
            //插入待插入元素,即最後一個值比它大的那個位置
            array[fast + step] = insertion;
        }
    }
複製代碼

以上代碼關鍵是 sortShell 方法的實現,雖然多拆了個插入排序的子方法,不過仍是比較容易理解的!

希爾排序的大致邏輯如此,不過上面代碼爲便於理解寫得實在囉嗦,下面咱們來寫下跟以上寫法等價的精簡版本:

/** * @see: 希爾排序的 Java 實現,精簡版 * @param array: 待排序數組,咱們採用原地排序 */
    public static void sortShell__(int[] array){
        //步長迭代,步長越大,分的子數組越多。步長爲1時只有一個子數組,就是原數組自己,步長最小爲 1
        for (int step = array.length/2; step >= 1; step /= 2){
            //開始對當前步長下全部子數組進行插入排序排序,千萬注意子數組是按步長分出來的,不是連續地分出來的
            //子數組從第二個元素開始遍歷,等於步長的下標即爲子數組第二個元素
            //下面這種寫法循環嵌套少了一層,實際上是交替排序每一個子數組(同時開始插入排序各個子數組),與上面囉嗦的寫法是等價的其實
            for (int slow = step; slow < array.length; slow ++){
                //待從新插入元素 array[slow]
                int insertion = array[slow];
                //內循環遍歷,主要爲肯定待插入元素array[slow]的待插入位置
                int fast = slow - step;
                for (; fast >= 0; fast -= step){
                    if (array[fast] > insertion){
                        array[fast + step] = array[fast];
                    }else {
                        //待插入元素的待插入位置,老是從後往前看,最後一個值比它大的那個位置,
                        // 值比它大的那些值總體日後移動一個位置
                        break;
                    }
                }
                //插入待插入元素,即最後一個值比它大的那個位置
                array[fast + step] = insertion;
            }
        }
    }
複製代碼

結尾

希爾排序真的比插入排序更高效嘛?光看代碼可能一頭霧水,做者不打算在這裏聊太多數學以證實希爾排序確實比插入排序更高效。事實是希爾排序確實比插入排序更高效,這點讀者可結合插入排序的代碼運行些測試用例自行對比。

希爾排序最重要的地方在於當用較小步長排序後,之前用較大步長的排序結果還是有序的。好比,若是一個數列以步長5進行了排序而後再以步長3進行排序,那麼該數列不只是以步長3有序,並且是以步長5有序。若是不是這樣,那麼算法在迭代過程當中會打亂之前的順序,那就不會以如此短的時間完成排序了。

關於希爾排序的步長序列選擇,上面代碼所使用的使用數組長度除以 2 迭代至 1 是效率最佳的選擇嗎?

不是的。

希爾排序步長序列選擇的問題就比較複雜了,本文且略過不談,讀者可求助偉大的互聯網自行研究此問題!

下篇,咱們聊聊堆排序,和優先隊列。

完。

相關文章
相關標籤/搜索