概述
希爾排序(Shell Sort)是 D.L.Shell 於 1959 年提出來的一種排序算法,在這之前排序算法的時間複雜度基本都是 O() 的,希爾排序算法是突破這個時間複雜度的第一批算法之一。希爾排序是一種插入排序算法。
版權說明
著作權歸作者所有。
商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
本文作者:Q-WHai
發表日期: 2016年4月12日
本文鏈接:https://qwhai.blog.csdn.net/article/details/51127533
來源:CSDN
更多內容:分類 >> 算法與數學
目錄
算法簡介
希爾排序(Shell Sort)是 D.L.Shell 於 1959 年提出來的一種排序算法,在這之前排序算法的時間複雜度基本都是 O() 的,希爾排序算法是突破這個時間複雜度的第一批算法之一。
– 《大話數據結構》
算法原理分析
在上一篇《排序算法系列:插入排序算法》一文中,我們說到了一種複雜度爲 O() 的排序算法。而對於插入排序而言,如果我們的排序序列基本有序,或是數據量比較小,那麼它的排序性能還是不錯的。爲什麼?可能你會這樣問。數據量小,這個不用多說,對於多數排序算法這一點都適用;如果一個序列已經基本有序(序列中小數普遍位於大數的前面),那麼我們在插入排序的時候,就可以在較少的比較操作之後使整體有序。如果,這一點你還不是很明白,可以先看看《排序算法系列:插入排序算法》一文中的解釋。
基於上面基本有序和小數據量的提示,這裏就有了希爾排序的實現。希爾所做的就是分組,在每個分組中進行插入排序。如下就是希爾排序中分組的示意圖:

在上圖中,我們將一個長度爲 10 的序列,分組成 3 組。除最後一組以外的分組都包含了 4 個元素。可是,我們排序的時候並不是在這些分組內部進行的。而是我們要按照上面圓形的標號來進行插入排序,也就是排序中的 [4, 9, 7]。你可以先想一想這是爲什麼,在文章的最後,我再說明這個問題。
上面從詳細地說明了希爾排序的由來及希爾排序的分組精髓。那麼現在就要說希爾排序中的插入排序,是的沒有錯。畢竟希爾排序的核心就是分組+插入排序嘛。在這個示意圖中,我們可以很明顯地看出它想表達的東西。這裏只是將[3, 9, 7]看成了一個整體,對於其它的元素,暫時與[3, 9, 7]無關。
通過上面的兩步操作,我們可以得到一個序列 T1 = [4, 0, 6, 1, 7, 2, 8, 5, 9, 3]。咦,這個序列還是無序的呀,怎麼回事?我們說希爾排序的核心是分組和獲得一個基本有序的數組。這裏說的是基本有序,並未做出承諾說這個序列已經有序。那麼,現在要做的就是繼續分組+插入排序。當然,對應的 step 是要進行修改的。詳細過程,參見下面的算法步驟。
算法步驟
通過上面的算法原理分析,這裏可以總結出算法實現的步驟。如下:
- 獲得一個無序的序列 T0 = [4, 3, 6, 5, 9, 0, 8, 1, 7, 2],並計算出當前序列狀態下的步長 step = length / 3 + 1;
- 對序列 T0 按照步長週期分組;
- 對每個子序列進行插入排序操作;
- 判斷此時的步長 step 是否大於 1?如果比 1 小或是等於 1,停止分組和對子序列的插入排序,否則,就繼續;
- 修改此時的步長 step = step / 3 + 1,並繼續第 2 到第 4 步;
- 排序算法結束,序列已是一個整體有序的序列。
邏輯實現
這是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) | 不穩定 | 較複雜 |
問題解答
- 爲什麼我們排序的時候並不是在這些分組內部進行的?
答:這裏我們分組的目的在於要完成的是對整個序列的基本序列處理,那麼我們肯定想要讓這些分組的數據儘量地分散開。如果要針對每個分組內部進行插入排序,那麼之後的每次操作,都會是在內部進行的,這樣的結果就是每個分組內部已經有序,只是整體仍然是無序的。 - 分組步長的計算公式爲什麼是 step = step / 3 + 1?
答:這裏的步長計算很關鍵,可是究竟應該選取什麼樣的增量纔是最好的,目前還尚未解決。不過大量的研究表明,當增量序列爲 dlta[k] = - 1 (0 <= k <= t <= [(n+1)])時,可以獲得不錯的效果,其時間複雜度爲 O()。
Ref
- 《大話數據結構》
Github源碼下載
- https://github.com/qwhai/algorithms-sort
徵集
如果你也需要使用ProcessOn這款在線繪圖工具,可以使用如下邀請鏈接進行註冊:
https://www.processon.com/i/56205c2ee4b0f6ed10838a6d