排序算法系列:Shell 排序算法

概述

希爾排序(Shell Sort)是 D.L.Shell 於 1959 年提出來的一種排序算法,在這之前排序算法的時間複雜度基本都是 O(n2n^{2}) 的,希爾排序算法是突破這個時間複雜度的第一批算法之一。希爾排序是一種插入排序算法。


版權說明

著作權歸作者所有。
商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
本文作者:Q-WHai
發表日期: 2016年4月12日
本文鏈接:https://qwhai.blog.csdn.net/article/details/51127533
來源:CSDN
更多內容:分類 >> 算法與數學


目錄

算法簡介

希爾排序(Shell Sort)是 D.L.Shell 於 1959 年提出來的一種排序算法,在這之前排序算法的時間複雜度基本都是 O(n2n^{2}) 的,希爾排序算法是突破這個時間複雜度的第一批算法之一。
                                              – 《大話數據結構》


算法原理分析

在上一篇《排序算法系列:插入排序算法》一文中,我們說到了一種複雜度爲 O(n2n^{2}) 的排序算法。而對於插入排序而言,如果我們的排序序列基本有序,或是數據量比較小,那麼它的排序性能還是不錯的。爲什麼?可能你會這樣問。數據量小,這個不用多說,對於多數排序算法這一點都適用;如果一個序列已經基本有序(序列中小數普遍位於大數的前面),那麼我們在插入排序的時候,就可以在較少的比較操作之後使整體有序。如果,這一點你還不是很明白,可以先看看《排序算法系列:插入排序算法》一文中的解釋。
基於上面基本有序和小數據量的提示,這裏就有了希爾排序的實現。希爾所做的就是分組,在每個分組中進行插入排序。如下就是希爾排序中分組的示意圖:
這裏寫圖片描述
在上圖中,我們將一個長度爲 10 的序列,分組成 3 組。除最後一組以外的分組都包含了 4 個元素。可是,我們排序的時候並不是在這些分組內部進行的。而是我們要按照上面圓形的標號來進行插入排序,也就是排序中的 [4, 9, 7]。你可以先想一想這是爲什麼,在文章的最後,我再說明這個問題。

上面從詳細地說明了希爾排序的由來及希爾排序的分組精髓。那麼現在就要說希爾排序中的插入排序,是的沒有錯。畢竟希爾排序的核心就是分組+插入排序嘛。在這個示意圖中,我們可以很明顯地看出它想表達的東西。這裏只是將[3, 9, 7]看成了一個整體,對於其它的元素,暫時與[3, 9, 7]無關。
這裏寫圖片描述

通過上面的兩步操作,我們可以得到一個序列 T1 = [4, 0, 6, 1, 7, 2, 8, 5, 9, 3]。咦,這個序列還是無序的呀,怎麼回事?我們說希爾排序的核心是分組和獲得一個基本有序的數組。這裏說的是基本有序,並未做出承諾說這個序列已經有序。那麼,現在要做的就是繼續分組+插入排序。當然,對應的 step 是要進行修改的。詳細過程,參見下面的算法步驟。


算法步驟

通過上面的算法原理分析,這裏可以總結出算法實現的步驟。如下:

  1. 獲得一個無序的序列 T0 = [4, 3, 6, 5, 9, 0, 8, 1, 7, 2],並計算出當前序列狀態下的步長 step = length / 3 + 1;
  2. 對序列 T0 按照步長週期分組;
  3. 對每個子序列進行插入排序操作;
  4. 判斷此時的步長 step 是否大於 1?如果比 1 小或是等於 1,停止分組和對子序列的插入排序,否則,就繼續;
  5. 修改此時的步長 step = step / 3 + 1,並繼續第 2 到第 4 步;
  6. 排序算法結束,序列已是一個整體有序的序列。

邏輯實現

這是Shell 排序的核心模塊,也是分組的關鍵步驟

private void core(int[] array) {
        int arrayLength = array.length;
        int step = arrayLength;
        do {
            step = step / 3 + 1;
            for (int i = 0; i < step; i++) {
                insert(array, i, step);
            }
            System.err.println(Arrays.toString(array));
        } while (step > 1);
    }

希爾排序的直接插入排序,注意這裏不同的是它只是針對某一個分組子序列進行插入排序

private void insert(int[] array, int offset, int step) {
        int arrayLength = array.length;
        int groupCount = arrayLength / step + (arrayLength % step > offset ? 1 : 0);
        for (int i = 1; i < groupCount; i++) {
            int nextIndex = offset + step * i;
            int waitInsert = array[nextIndex];
            
            while(nextIndex - step >= 0 && waitInsert < array[nextIndex - step]) {
                array[nextIndex] = array[nextIndex - step];
                nextIndex -= step;
            }
            
            array[nextIndex] = waitInsert;
        }
    }

更多詳細邏輯實現,請參見文章結尾的源碼鏈接。


複雜度分析

排序方法 時間複雜度 空間複雜度 穩定性 複雜性
平均情況 最壞情況 最好情況
Shell 排序 O($n^{3/2}$) O($n^{2}$) O(n) O(n) 不穩定 較複雜

問題解答

  1. 爲什麼我們排序的時候並不是在這些分組內部進行的?
    答:這裏我們分組的目的在於要完成的是對整個序列的基本序列處理,那麼我們肯定想要讓這些分組的數據儘量地分散開。如果要針對每個分組內部進行插入排序,那麼之後的每次操作,都會是在內部進行的,這樣的結果就是每個分組內部已經有序,只是整體仍然是無序的。
  2. 分組步長的計算公式爲什麼是 step = step / 3 + 1?
    答:這裏的步長計算很關鍵,可是究竟應該選取什麼樣的增量纔是最好的,目前還尚未解決。不過大量的研究表明,當增量序列爲 dlta[k] = 2tk+1{2^{t-k+1}} - 1 (0 <= k <= t <= [log2log_{2}(n+1)])時,可以獲得不錯的效果,其時間複雜度爲 O(n3/2n^{3/2})。

Ref

  • 《大話數據結構》

Github源碼下載

  • https://github.com/qwhai/algorithms-sort

徵集

如果你也需要使用ProcessOn這款在線繪圖工具,可以使用如下邀請鏈接進行註冊:
https://www.processon.com/i/56205c2ee4b0f6ed10838a6d

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