聲明:算法和數(shù)據(jù)結(jié)構(gòu)的文章均是作者從github上翻譯過(guò)來(lái),為方便大家閱讀。如果英語(yǔ)閱讀能力強(qiáng)的朋友,可以直接到swift算法俱樂(lè)部查看所有原文,以便快速學(xué)習(xí)。作者同時(shí)也在學(xué)習(xí)中,歡迎交流
希爾排序是基于插值排序的算法,通過(guò)將原始數(shù)據(jù)分為總數(shù)更小的幾個(gè)小數(shù)組,并在這些小數(shù)組中整理排序的方式來(lái)提升插值排序的性能。
這里有一個(gè)模仿希爾算法執(zhí)行過(guò)程的表演-匈牙利某大學(xué)傳統(tǒng)舞蹈小視屏
執(zhí)行原理
與插值排序通過(guò)對(duì)比相鄰的兩個(gè)元素的大小并在必要時(shí)候交換位置,希爾排序是通過(guò)比較相隔很遠(yuǎn)的兩個(gè)元素。
兩個(gè)元素之間的距離稱(chēng)為間隔。如果兩個(gè)元素在比較之后需要交換位置,則直接更換彼此的位置。這個(gè)過(guò)程減少了插值排序中很多不必要的中間復(fù)制過(guò)程,即從兩個(gè)元素更換位置前需要不斷交換相鄰元素的位置直到目的位置。
這里的最主要的思想就是,元素通過(guò)每次移動(dòng)較大間隔,整個(gè)數(shù)組可以快速形成局部排序好的情況。這個(gè)會(huì)讓接下來(lái)的交換變得更加快速。因?yàn)樵刂g不需要進(jìn)行過(guò)多次的位置交換。
一旦某一距離長(zhǎng)度的間隔比值交換完成,間隔會(huì)變得越來(lái)越小,然后進(jìn)行相應(yīng)間隔的比值交換,這樣的過(guò)程不斷重復(fù),直到間隔為1,也就是與插值排序同樣過(guò)程的情況。然而,在希爾排序中,由于大部分?jǐn)?shù)據(jù)在此時(shí)已經(jīng)整理完畢,所以最后間隔為1的比值交換速度非常快。
例子
假設(shè)我們要用希爾排序?qū)?shù)組[64, 20, 50, 33, 72, 10, 23, -1, 4]
進(jìn)行整理。
我們從間隔為數(shù)組長(zhǎng)度二分一開(kāi)始:
n = floor(9/2) = 4
我們創(chuàng)建n個(gè)子數(shù)組。在每一個(gè)數(shù)組中,不同元素之間的間隔距離為n。在我們的例子中中,我們需要?jiǎng)?chuàng)建4個(gè)這樣的數(shù)組。這些數(shù)據(jù)會(huì)通過(guò)insertionSort()
函數(shù)進(jìn)行整理排序。我們可以通過(guò)圖表進(jìn)行深入了解:
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 ]
如圖所示,每一個(gè)子數(shù)組里面只包含原數(shù)組中每一個(gè)第4個(gè)元素。其他非第4元素的用xx表示。所以第一個(gè)子數(shù)組為[64,72,4]
,第二個(gè)為[20,10]
,以此類(lèi)推。這里我們使用間隔
的原因是我們不需要直接創(chuàng)建新的數(shù)組,所有的交換過(guò)程都在原始數(shù)組中完成。
現(xiàn)在我們開(kāi)始使用insertionSort()
函數(shù)進(jìn)行每個(gè)子數(shù)組的整理排序。比如第一個(gè)子數(shù)組中,我們需要先將4和72交換位置,然后是4和64,然后72大于64不需要交換。整理后的第一個(gè)子數(shù)組為:
sublist 0: [ 4, xx, xx, xx, 64, xx, xx, xx, 72 ]
其他子數(shù)組也完成同樣過(guò)程,得到結(jié)果如下:
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 ]
此時(shí),從原數(shù)組中看,結(jié)果是這樣的:
[ 4, 10, 23, -1, 64, 20, 50, 33, 72 ]
現(xiàn)階段并不是完全整理好的,但是對(duì)比最早時(shí)候的數(shù)據(jù),已經(jīng)相對(duì)有序的多。現(xiàn)在第一次比值交換結(jié)束,我們開(kāi)始進(jìn)行第二次交換。將第一次的交換間隔除以2,得到第二次間隔2.
n = floor(4/2) = 2
這也意味著我們這次只需要?jiǎng)?chuàng)建兩個(gè)子數(shù)組。
sublist 0: [ 4, xx, 23, xx, 64, xx, 50, xx, 72 ]
sublist 1: [ xx, 10, xx, -1, xx, 20, xx, 33, xx ]
每個(gè)子數(shù)組包含間隔2的元素。重復(fù)之前的步驟,我們繼續(xù)使用insertionSort()
函數(shù)進(jìn)行每個(gè)子數(shù)組的整理排序。結(jié)果如下:
sublist 0: [ 4, xx, 23, xx, 50, xx, 64, xx, 72 ]
sublist 1: [ xx, -1, xx, 10, xx, 20, xx, 33, xx ]
通過(guò)觀察我們可以發(fā)現(xiàn),每一個(gè)子數(shù)組中均只有2個(gè)元素不在正確的位置上,所以在這一次的插值排序速度很快。
此時(shí)的原始數(shù)組為:
[ 4, -1, 23, 10, 50, 20, 64, 33, 72 ]
到這里第二次比值交換也結(jié)束,我們只需要進(jìn)行最后一次比值交換,間隔為1:
n = floor(2/2) = 1
這也意味著這次的子數(shù)組個(gè)數(shù)為1,可以直接對(duì)當(dāng)前的數(shù)組進(jìn)行整理,繼續(xù)使用insertionSort()
函數(shù)。結(jié)果如下:
[ -1, 4, 10, 20, 23, 33, 50, 64, 72 ]
對(duì)希爾排序算法來(lái)說(shuō),大部分情況下它的性能是O(n^2),當(dāng)然運(yùn)氣好的時(shí)候是 O(n log n)。需要注意的是,希爾排序算法得到的是不穩(wěn)定序列,它可能會(huì)對(duì)數(shù)值相同的兩個(gè)元素進(jìn)行位置交換。
間隔序列
間隔序列決定了間隔的初始值以及每次迭代過(guò)程中新的間隔值。對(duì)于希爾排序算法來(lái)說(shuō),一個(gè)好的間隔序列可以讓整個(gè)算法表現(xiàn)的更好。
間隔序列的取值方法不是唯一的,在我們文中,我們采用的是整理數(shù)組個(gè)數(shù)的二分一,然后每次迭代繼續(xù)除以二分一的策略。
代碼
var arr = [64, 20, 50, 33, 72, 10, 23, -1, 4, 5]
public func shellSort(_ list: inout [Int]) {
var sublistCount = list.count / 2
while sublistCount > 0 {
for index in 0..<list.count {
guard index + sublistCount < list.count else { break }
if list[index] > list[index + sublistCount] {
swap(&list[index], &list[index + sublistCount])
}
guard sublistCount == 1 && index > 0 else { continue }
if list[index - 1] > list[index] {
swap(&list[index - 1], &list[index])
}
}
sublistCount = sublistCount / 2
}
}
shellSort(&arr)