本文是對 Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Club是 raywenderlich.com網站出品的用Swift實現演算法和資料結構的開源專案,目前在GitHub上有18000+⭐️,我初略統計了一下,大概有一百左右個的演算法和資料結構,基本上常見的都包含了,是iOSer學習演算法和資料結構不錯的資源。
?andyRon/swift-algorithm-club-cn是我對Swift Algorithm Club,邊學習邊翻譯的專案。由於能力有限,如發現錯誤或翻譯不妥,請指正,歡迎pull request。也歡迎有興趣、有時間的小夥伴一起參與翻譯和學習?。當然也歡迎加⭐️,?????。
本文的翻譯原文和程式碼可以檢視?swift-algorithm-club-cn/Shell Sort
希爾排序(Shell Sort)
希爾排序是插入排序的一種更高效的改進版本,方法是將原始列表分成較小的子列表,然後使用插入排序對其進行單獨排序。
Sapientia大學建立了一個很好的視訊,顯示了匈牙利民間舞蹈的過程。(譯註:類似希爾排序的過程,油管視訊需要翻牆)
怎麼執行的
插入排序是比較相連的元素,如果它們順序不對就交換它們,而希爾排序演算法會比較相距很遠的元素。
元素之間的距離稱為 gap。 如果被比較的元素的順序錯誤,則它們會在 gap 中交換。 這消除了插入排序中常見的許多中間副本。
譯註: gap已經被翻譯成步長/增量/間距等,為了避免歧義,本文就不做翻譯,直接寫成gap
這個想法是,通過在大 gap 上移動元素,陣列變得非常快速地部分排序。 這使得之後的排序過程更快,因為他們不再需要交換那麼多項。
一輪完成後,gap變小,新一輪開始。 這將重複,直到 gap 大小為1,此時演算法的功能就像插入排序一樣。 但是由於資料已經很好地排序,所以最後一輪可以非常快。
例子
假設我們想使用希爾排序對陣列 [64, 20, 50, 33, 72, 10, 23, -1, 4]
進行排序。
我們首先將陣列的長度除以2:
n = floor(9/2) = 4
複製程式碼
這是 gap 大小。
我們建立n
子列表。 在每個子列表中,每一項的間隔是大小為n
的gap 。 在我們的示例中,我們需要製作其中四個子列表。 子列表按insertionSort()
函式排序。
這可能沒有多大意義,所以讓我們仔細看看會發生什麼。
第一輪如下。 我們有n = 4
,所以我們製作了四個子列表:
sublist 0: [ 64, xx, xx, xx, 72, xx, xx, xx, 4 ]
sublist 1: [ xx, 20, xx, xx, xx, 10, xx, xx, xx ]
sublist 2: [ xx, xx, 50, xx, xx, xx, 23, xx, xx ]
sublist 3: [ xx, xx, xx, 33, xx, xx, xx, -1, xx ]
複製程式碼
如您所見,每個子列表僅包含原始陣列中的每間隔4的項。 不在子列表中的項用xx
表示。 所以第一個子列表是[64,72,4]
,第二個子列表是[20,10]
,依此類推。 我們使用這個“gap”的原因是我們不必實際製作新的陣列。 相反,我們將它們交織在原始陣列中。
我們現在在每個子列表上呼叫一次insertionSort()
。
插入排序的這個特定版本從後面到前面排序。子列表中的每個專案都與其他專案進行比較。如果它們的順序錯誤,則交換值並一直向下移動,直到我們到達子列表的開頭。
因此對於子列表0,我們將4
與72
交換,然後將4
與64
交換。 排序後,此子列表如下所示:
sublist 0: [ 4, xx, xx, xx, 64, xx, xx, xx, 72 ]
複製程式碼
排序後的其他三個子列表:
sublist 1: [ xx, 10, xx, xx, xx, 20, xx, xx, xx ]
sublist 2: [ xx, xx, 23, xx, xx, xx, 50, xx, xx ]
sublist 3: [ xx, xx, xx, -1, xx, xx, xx, 33, xx ]
複製程式碼
完整的陣列看上去是:
[ 4, 10, 23, -1, 64, 20, 50, 33, 72 ]
複製程式碼
它還沒有完全排序,但它比以前更加排序。 這完成了第一次輪操作。
在第二輪中,我們將 gap 大小除以2:
n = floor(4/2) = 2
複製程式碼
這意味著我們現在只建立兩個子列表:
sublist 0: [ 4, xx, 23, xx, 64, xx, 50, xx, 72 ]
sublist 1: [ xx, 10, xx, -1, xx, 20, xx, 33, xx ]
複製程式碼
每個子列表包含每個間隔為2的項。 我們再次呼叫insertionSort()
來對這些子列表進行排序。 結果是:
sublist 0: [ 4, xx, 23, xx, 50, xx, 64, xx, 72 ]
sublist 1: [ xx, -1, xx, 10, xx, 20, xx, 33, xx ]
複製程式碼
請注意,在每個列表中只有兩個元素位置順序不對(譯註:sublist 0是64和50,sublist 1是10和-1)。 因此插入排序非常快。 那是因為我們已經在第一輪中對陣列進行了一些排序。
總陣列現在看起來像這樣:
[ 4, -1, 23, 10, 50, 20, 64, 33, 72 ]
複製程式碼
這樣就完成了第二輪。 最後一輪的gap是:
n = floor(2/2) = 1
複製程式碼
gap 大小為1表示我們只有一個子列表,即陣列本身,我們再次呼叫insertionSort()
對其進行排序。 最終排序的陣列是:
[ -1, 4, 10, 20, 23, 33, 50, 64, 72 ]
複製程式碼
在大多數情況下,希爾排序的效能為O(n^2),如果幸運,則為 O(nlogn)。 該演算法是不穩定的排序; 它可能會改變具有相等值的元素的相對順序。
gap 序列
“ gap 序列”確定 gap 的初始大小以及每次迭代如何使 gap 變小。 良好的 gap 序列對於希爾排序表現良好非常重要。
上面實現例子中的 gap 序列是希爾原始版本中的 gap 序列:初始值是陣列大小的一半,然後每次除以2。 還有其他方法可以計算 gap 序列。
只是為了好玩...
這是 Matthijs 很久以前使用的一箇舊的Commodore 64 BASIC版本的希爾排序,並且移植到他曾經使用的幾乎所有程式語言中:
61200 REM S is the array to be sorted
61205 REM AS is the number of elements in S
61210 W1=AS
61220 IF W1<=0 THEN 61310
61230 W1=INT(W1/2): W2=AS-W1
61240 V=0
61250 FOR N1=0 TO W2-1
61260 W3=N1+W1
61270 IF S(N1)>S(W3) THEN SH=S(N1): S(N1)=S(W3): S(W3)=SH: V=1
61280 NEXT N1
61290 IF V>0 THEN 61240
61300 GOTO 61220
61310 RETURN
複製程式碼
程式碼
希爾排序的Swift實現:
public func insertSort(_ list: inout[Int], start: Int, gap: Int) {
for i in stride(from: (start + gap), to: list.count, by: gap) {
let currentValue = list[I]
var pos = I
while pos >= gap && list[pos - gap] > currentValue {
list[pos] = list[pos - gap]
pos -= gap
}
list[pos] = currentValue
}
}
public func shellSort(_ list: inout [Int]) {
var sublistCount = list.count / 2
while sublistCount > 0 {
for pos in 0..<sublistCount {
insertionSort(&list, start: pos, gap: sublistCount)
}
sublistCount = sublistCount / 2
}
}
var arr = [64, 20, 50, 33, 72, 10, 23, -1, 4, 5]
shellSort(&arr)
複製程式碼
擴充套件閱讀
Rosetta code的希爾排序(譯註:大概70種不同語言實現希爾排序??)
作者:Mike Taghavi,Matthijs Hollemans
翻譯:Andy Ron
校對:Andy Ron
翻譯後補充
希爾排序,也稱遞減增量排序演算法,按其設計者希爾(Donald Shell)的名字命名,在1959年公佈。
定義
希爾排序是將待排序的陣列元素按下標的一定增量分組 ,分成多個子序列,然後對各個子序列進行直接插入排序演算法排序;然後依次縮減增量再進行排序,直到增量為1時,進行最後一次直接插入排序,排序結束。
希爾排序的原理圖
- 圖片一來源:圖解排序演算法(二)之希爾排序
- 圖片二來源:排序:希爾排序(演算法)
-
一個希爾排序的動畫