希爾排序(Shell Sort)
上回說到,三大基本排序氣泡排序、選擇排序和插入排序。
其中插入排序又叫直接插入排序,其核心思想是通過構建有序序列,對未排序序列中選出首位資料,從已排序序列從後向前掃描,找到相應位置並插入。直接插入排序對小規模資料或基本有序資料十分高效。
希爾排序,1959年由Donald Shell發明,他是第一個突破O(n²)的排序演算法。希爾排序是直接插入排序的改進版(你問我為什麼不和直接插入排序寫在一起?博主太笨了,希爾排序研究了兩天才理解其中繞來繞去的迴圈--)。
希爾排序將序列分割成若干小序列(邏輯上分組),對每一個小序列進行插入排序,此時每一個小序列資料量小,插入排序的效率也提高了。
演算法描述
- 選擇一個增量序列,t1,t2....tk,其中ti>tj,tk = 1;
- 按增量序列個數k,對序列進行k次排序;
- 每次排序,根據增量ti,將待排序序列分成若干子序列,分別對子序列進行直接插入排序。當增量為tk也就是1時,進行最後一次排序,此時子序列為排序序列本身。
動圖演示
(好吧_(¦3」∠)_,這個圖太抽象了,得多看12345678遍)
程式碼實現
先來看另一篇部落格的運算圖:
該圖按下標距離為4進行分組,arr[0]和arr[4]為一組,arr[1]和arr[5]為一組,這裡的下標距離4就被稱為增量
。
對四個子序列進行插入排序之後:
此時四個子序列都是有序的,陣列變為:
然後縮小增量為上個增量的一半:2,繼續劃分分組,此時,每個分組元素個數多了,但是,陣列變的部分有序了,插入排序效率同樣比較高
最後設定增量為上一個增量的一半:1,則整個陣列被分為一組,此時,整個陣列已經接近有序了:
如果看到這裡你還不懂的話。。。。。。那你跟我一樣笨,繼續再看12345678遍就好了O(∩_∩)O。
let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
let len = arr.length;
let willInsertValue;
let gap = len; // 定義增量
// 動態定義增量序列,每一次增量變為上次一半,最後一次的gap為1
while(gap>0&&(gap = Math.trunc(gap/2))){
// 對每個分組進行插入排序,為什麼i開始是gap,因為插入排序預設第一位是已排序序列,arr[gap]是第一個分組第二位
for(let i = gap;i<len;i++){
// 待進行插入的值為a[i]
willInsertValue = arr[i];
// 按組進行插入,這裡比較繞人,前面說了,只是邏輯上的分組,實際上還是一個序列,這裡按組插入的時候是交叉
// 進行插入
let j = i - gap;
// 下面就是個直接插入排序,只不過每次移位的時候下標差值為gap
while(j>=0&&arr[j]>willInsertValue){
arr[j+gap] = arr[j];
j -= gap
}
arr[j+gap] = willInsertValue
}
}
複製程式碼
輸出結果為:
分析一下複雜度:
空間複雜度依然是O(1)
Shell排序的執行時間依賴於增量序列。
好的增量序列的共同特徵:
- 最後一個增量必須為1;
- 應該儘量避免序列中的值(尤其是相鄰的值)互為倍數的情況。
這樣來看的話,上述栗子選擇的增量1,2,4這樣的其實並不是很好,使用這種增量序列時間複雜度最壞為O(n平方)。
Hibbard提出了另一個增量序列{1,3,7,...,2^k-1},這種序列的時間複雜度(最壞情形)為O(n^1.5)
Sedgewick提出了幾種增量序列,其最壞情形執行時間為O(n^1.3),其中最好的一個序列是{1,5,19,41,109,...}
對於結果來說,使用哪種增量都沒有影響,只要最後一次的增量變為1即可。上述栗子增量不能大於待排序序列的長度,否則gap為0,無法進行排序。
希爾排序又叫縮小增量排序。
最後說一下穩定性,由於希爾排序是交叉跳躍排序,所以是不穩定的排序。
總結
個人感覺在學習希爾排序的時候,難點在於最後交叉跳躍進行插入排序,前面說了是在邏輯上進行分組,思維完全被分組限制住了,總想著排序的時候也是按組排序,其實是以大序列的順序對子序列進行跳躍式排序。
適用場景
希爾排序是對直接插入排序的一種優化,可以用於大型的陣列,希爾排序比插入排序和選擇排序要快的多,並且陣列越大,優勢越大。