1. 程式人生 > 實用技巧 >十大經典排序演算法 ( 四 ) 希爾排序

十大經典排序演算法 ( 四 ) 希爾排序

介紹 :

  希爾排序(Shell's Sort)是插入排序的一種又稱“縮小增量排序”(Diminishing Increment Sort),是直接插入排序演算法的一種更高效的改進版本。希爾排序是非穩定排序演算法。該方法因 D.L.Shell 於 1959 年提出而得名。

  希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
  1. 插入排序在對幾乎已經排好序的資料操作時,效率高,即可以達到線性排序的效率。
  2. 但插入排序一般來說是低效的,因為插入排序每次只能將資料移動一位。

演算法原理 :

  先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。因為直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率上比前兩種方法有較大提高。

動圖演示 :

演算法穩定性 :

  由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的。

時間複雜度 :
  希爾排序是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小,插入排序對於有序的序列效率很高。所以,希爾排序的時間複雜度會比 o( n^2 ) 好一些。

  1.增量序列的選擇     Shell排序的執行時間依賴於增量序列。     好的增量序列的共同特徵:       ① 最後一個增量必須為1;       ② 應該儘量避免序列中的值(尤其是相鄰的值)互為倍數的情況。     有人通過大量的實驗,給出了較好的結果:當n較大時,比較和移動的次數約在n^1.25到(1.6n)^1.25之間。   2.Shell排序的時間效能優於直接插入排序
    希爾排序的時間效能優於直接插入排序的原因:       ①當檔案初態基本有序時直接插入排序所需的比較和移動次數均較少。       ②當n值較小時,n 和的差別也較小,即直接插入排序的最好時間複雜度O(n)和最壞時間複雜度 0() 差別不大。       ③在希爾排序開始時增量較大,分組較多,每組的記錄數目少,故各組內直接插入較快,後來增量di逐漸縮小,分組數逐漸減少,而各組的記錄數目逐漸增多,但由於已經按di-1作為距離排過序,使檔案較接近於有序狀態,所以新的一趟排序過程也較快。   因此,希爾排序在效率上較直接插入排序有較大的改進。

  希爾排序的複雜度和增量序列是相關的

  {1,2,4,8,...}這種序列並不是很好的增量序列,使用這個增量序列的時間複雜度(最壞情形)是O(n^2)

  Hibbard提出了另一個增量序列{1,3,7,...,2^k-1},這種序列的時間複雜度(最壞情形)為O(n^1.5)

  Sedgewick提出了幾種增量序列,其最壞情形執行時間為O(n^1.3),其中最好的一個序列是{1,5,19,41,109,...}

應用場景 :

  專家們提倡,幾乎任何排序工作在開始時都可以用希爾排序,若在實際使用中證明它不夠快,再改成快速排序這樣更高階的排序演算法。因為對於中等大小的陣列它的執行時間是可以接受的。 它的程式碼量很小,且不需要使用額外的記憶體空間。雖然有更加高效的演算法,但 除了對於很大的 N,它們可能只會比希爾排序快兩倍(可能還達不到),而且更復雜。如果你需要 解決一個排序問題而又沒有系統排序函式可用(例如直接接觸硬體或是運行於嵌入式系統中的代 碼),可以先用希爾排序,然後再考慮是否值得將它替換為更加複雜的排序演算法。

Java程式碼 :

/**
 * 希爾排序 : 把陣列按下標的一定增量分組,對每組使用直接插入排序演算法排序;隨著增量逐漸減少,每組包含的元素越來越多,當增量減至 1 時,整個陣列恰被分成一組,演算法便終止。
 */
public class ShellSort {

    public static void sort(int[] arr) {
        // 判斷邊界條件
        if (arr == null || arr.length < 2) {
            return;
        }
        int gap = 1;// 增量
        while (gap < arr.length) {
            gap = gap * 5 + 1;// 增量序列 : 1 , 4 , 13 , 40 , 121 , 364 , 1093...
        }
        // 進行分組
        while (gap > 0) {
            // 對各個分組進行 插入排序
            for (int i = gap; i < arr.length; i++) {
                insertValue (arr, gap, i);// 將arr[i]插入到所在分組的正確位置上
            }
            gap = gap / 5;// gap收縮,直至1
        }
    }

    /**
     * 將arr[i]插入到所在分組的正確位置上 ( 程式碼與插入排序幾乎一致 )
     *
     * @param arr 陣列
     * @param gap 增量
     * @param i   陣列下標
     */
    private static void insertValue(int[] arr, int gap, int i) {
        int tmp = arr[i];
        int j = i - gap;
        // 按組進行插入(組內元素兩兩相隔gap)
        while (j >= 0 && arr[j] > tmp) {
            arr[j + gap] = arr[j];
            j -= gap;
            if (j + gap != i) {
                arr[j + gap] = tmp;
            }
        }
    }
}

如果還有沒看懂的同學 , 可以看看下面這個有大量圖解的部落格

https://blog.csdn.net/qq_39207948/article/details/80006224