排序算法基礎
排序算法,是一種能將一串數據按照特定的排序方式進行排列的一種算法,一個排序算法的好壞,主要從時間復雜度,空間復雜度,穩定性來衡量。
時間復雜度
時間復雜度是一個函數,它描述了該算法的運行時間,考察的是當輸入值大小趨近無窮時的情況。數學和計算機科學中使用這個大 O 符號用來標記不同”階“的無窮大。這里的無窮被認為是一個超越邊界而增加的概念,而不是一個數。
想了解時間復雜度,我想講講常見的 O(1),O(log n),O(n),O(n log n),O(n^2) ,計算時間復雜度的過程,常常需要分析一個算法運行過程中需要的基本操作,計量所有操作的數量。
O(1)常數階
O(1)中的 1 并不是指時間為 1,也不是操作數量為 1,而是表示操作次數為一個常數,不因為輸入 n 的大小而改變,比如哈希表里存放 1000 個數據或者 10000 個數據,通過哈希碼查找數據時所需要的操作次數都是一樣的,而操作次數和時間是成線性關系的,所以時間復雜度為 O(1)的算法所消耗的時間為常數時間。
O(log n)對數階
O(log n)中的 log n 是一種簡寫,loga n 稱作為以 a 為底 n 的對數,log n 省略掉了 a,所以 log n 可能是 log2 n,也可能是 log10 n。但不論對數的底是多少,O(log n)是對數時間算法的標準記法,對數時間是非常有效率的,例如有序數組中的二分查找,假設 1000 個數據查找需要 1 單位的時間, 1000,000 個數據查找則只需要 2 個單位的時間,數據量平方了但時間只不過是翻倍了。如果一個算法他實際的得操作數是 log2 n + 1000, 那它的時間復雜度依舊是 log n, 而不是 log n + 1000,時間復雜度可被稱為是漸近時間復雜度,在 n 極大的情況,1000 相對 與 log2 n 是極小的,所以 log2 n + 1000 與 log2 n 漸進等價。
O(n)線性階
如果一個算法的時間復雜度為 O(n),則稱這個算法具有線性時間,或 O(n) 時間。這意味著對于足夠大的輸入,運行時間增加的大小與輸入成線性關系。例如,一個計算列表所有元素的和的程序,需要的時間與列表的長度成正比。遍歷無序數組尋最大數,所需要的時間也與列表的長度成正比。
O(n log n)線性對數階
排序算法中的快速排序的時間復雜度即 O(n log n),它通過遞歸 log2n 次,每次遍歷所有元素,所以總的時間復雜度則為二者之積, 復雜度既 O(n log n)。
O(n^2)平方階
冒泡排序的時間復雜度既為 O(n^2),它通過平均時間復雜度為 O(n)的算法找到數組中最小的數放置在爭取的位置,而它需要尋找 n 次,不難理解它的時間復雜度為 O(n^2)。時間復雜度為 O(n^2)的算法在處理大數據時,是非常耗時的算法,例如處理 1000 個數據的時間為 1 個單位的時間,那么 1000,000 數據的處理時間既大約 1000,000 個單位的時間。
時間復雜度又有最優時間復雜度,最差時間復雜度,平均時間復雜度。部分算法在對不同的數據進行操作的時候,會有不同的時間消耗,如快速排序,最好的情況是 O(n log n),最差的情況是 O(n^2),而平均復雜度就是所有情況的平均值,例如快速排序計算平均復雜度的公式為
時間復雜度效率比較
除了上述所說的時間復雜度,下表中展示了其他一些時間復雜度,以及這些時間復雜度之間的比較。
想O(n^3)、O(n!)等這樣的時間復雜度,過大的n會使算法變得不現實,都是時間的噩夢,所以這種不切實際的復雜度,一般都不會去考慮這樣的算法。
空間復雜度
和時間復雜度一樣,有 O(1),O(log n),O(n),O(n log n),O(n^2),等等。實際寫代碼的過程中完全可以用空間來換取時間。比如判斷2017年之前的某一年是不是閏年,通常可以使通過一個算法來解決。但還有另外一種做法就是將2017年之前的閏年保存到一個數組中。如果某一年存在這個數組中就是閏年,反之就不是。一般來說時間復雜度和空間復雜度是矛盾的。到底優先考慮時間復雜度還是空間復雜度,取決于實際的應用場景。
穩定性
假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,ri = rj,且 ri 在 rj 之前,而在排序后的序列中,ri 仍在 rj 之前,則稱這種排序算法是穩定的;否則稱為不穩定的。
當相等的元素是無法分辨的,比如像是整數,穩定性并不是一個問題。然而,假設以下的數對將要以他們的第一個數字來排序。
(4, 1) (3, 1) (3, 7) (5, 6)
在這個狀況下,有可能產生兩種不同的結果,一個是讓相等鍵值的紀錄維持相對的次序,而另外一個則沒有:
(3, 1) (3, 7) (4, 1) (5, 6) (維持次序)
(3, 7) (3, 1) (4, 1) (5, 6) (次序被改變)
不穩定排序算法可能會在相等的鍵值中改變紀錄的相對次序,這導致我們無法準確預料排序結果(除非你把數據在你的大腦里用該算法跑一遍),但是穩定排序算法從來不會如此。例如冒泡排序即穩定的存在,相等不交換則不打亂原有順序。而快速排序有時候則是不穩定的。
常見排序算法
本文介紹6種常用排序算法,包括冒泡、選擇、插入、快排、堆排、希爾排序。下面從排序算法的原理、解題步驟、實現代碼三個方面去介紹排序。
冒泡排序
原理解析
引自維基百科冒泡排序(英語:Bubble Sort)是一種簡單的排序算法。它重復地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重復地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。
冒泡排序的理念十分簡單:不斷比較相鄰的兩個元素,如果它們的順序不符合要求就互相調換。
解題步驟
步驟如下:
* 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個,直到把最大的元素放到數組尾部。
* 遍歷長度減一,對剩下的元素從頭重復以上的步驟。
* 直到沒有任何一對數字需要比較時完成。
假如需要對 1 4 6 2 8 這五個數按照從大到小的排序進行排列,那么我們應該怎么入手解決呢?
首先,比較第 1 位數和第 2 位數的大小。很明顯 1 要小于 4,所以我們把 1 和 4 的位置互換一下。
然后,我們繼續比較第 2位數和第 3 位數,發現 1 要小于 6,因此把 1 和 6 的位置互換。
繼續比較第 3 位和第 4 位數,1 要小于 2,根據要求把 1 和 2 的位置互換。
最后比較第 4 位和第 5 位數,顯然 1 小于 8,同理把 1 和 8 的位置互換。
經過這樣一輪操作,我們已經不知不覺中把數字 1 的位置放好了,1 是這五個數字中數值最小的,應該排在最后一位。
我們回過頭來,看看剛剛才的排序過程,1 的位置經由交換慢慢“浮”到數列的頂端,是不是很像氣泡上浮的過程,這也是冒泡排序算法這個名字的由來。
第一輪操作結束后,我們把五個數字中數值最小的 1 擺放好了。第二輪操作我們將會把五個數字中數值第二小的 2 擺放好。仔細想想這個規律,是不是很有意思?同樣按照第一輪的規則進行操作,先比較第 1 位和第 2 位數,依此類推,過程如下。
實現代碼
func bubbleSort() {//冒泡排序
var list = [61,5,33,44,22]
for i in 0..<list.count {//找到符合條件的數進行交換
for j in i+1..<list.count {
if list[j] > list[i] {
let temp = list[j]
list[j] = list[i]
list[i] = temp
}
}
}
print(list)
}
最好的情況下,即待排序的數組本身是有序的,比較的次數是 n-1 次,沒有數據交換,時間復雜度為O(n)。最壞的情況下,即待排序的數組是完全逆序的情況,此時需要比較 n*(n-1)/2 次。因此時間復雜度是O(n^2)。
選擇排序
原理解析
引自維基百科選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再從剩余未排序元素中繼續尋找最小(大)元素,然后放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
選擇排序的基本思想就是通過 n-i 次關鍵字之間的比較,從n-i+1個記錄中選出關鍵字最小的記錄,并和第i個記錄做交換,而不是像冒泡排序那樣,每一次比較之后,符合條件的都做一次交換。選擇排序相對于冒泡排序做的交換次數更少。
解題步驟
步驟如下:
* 遍歷數組,找到最小的元素,將其置于數組起始位置。
* 從上次最小元素存放的后一個元素開始遍歷至數組尾,將最小的元素置于開始處。
* 重復上述過程,直到元素排序完畢。
以數組 arr = [8, 5, 2, 6, 9, 3, 1, 4, 0, 7] 為例,先直觀看一下每一步的變化,后面再介紹細節
第一次從數組 [8, 5, 2, 6, 9, 3, 1, 4, 0, 7] 中找到最小的數 0,放到數組的最前面(與第一個元素進行交換):
min
↓
8 5 2 6 9 3 1 4 0 7
↑ ↑
└───────────────────────────────┘
交換后:
0 5 2 6 9 3 1 4 8 7
在剩余的序列中 [5, 2, 6, 9, 3, 1, 4, 8, 7] 中找到最小的數 1,與該序列的第一個個元素進行位置交換:
min
↓
0 5 2 6 9 3 1 4 8 7
↑ ↑
└───────────────────┘
交換后:
0 1 2 6 9 3 5 4 8 7
在剩余的序列中 [2, 6, 9, 3, 5, 4, 8, 7] 中找到最小的數 2,與該序列的第一個個元素進行位置交換(實際上不需要交換):
min
↓
0 1 2 6 9 3 5 4 8 7
↑
重復上述過程,直到最后一個元素就完成了排序。
min
↓
0 1 2 6 9 3 5 4 8 7
↑ ↑
└───────┘
min
↓
0 1 2 3 9 6 5 4 8 7
↑ ↑
└───────────┘
min
↓
0 1 2 3 4 6 5 9 8 7
↑ ↑
└───┘
min
↓
0 1 2 3 4 5 6 9 8 7
↑
min
↓
0 1 2 3 4 5 6 9 8 7
↑ ↑
└───────┘
min
↓
0 1 2 3 4 5 6 7 8 9
↑
min
↓
0 1 2 3 4 5 6 7 8 9
↑
實現代碼
func chooseSort() {//選擇排序
var list = [61,5,33,44,22]
for i in 0..<list.count {
var min = i//記錄當前最小的數,比較i+1后更大的數進行記錄
for j in i+1..<list.count {
if list[j] < list[min] {
min = j
}
}
let temp = list[min]
list[min] = list[i]
list[i] = temp
}
print(list)
}
注意簡單選擇排序中的數據交換是放在第一層for循環內部,當尋找到目標下標才進行數據交換。而冒泡排序的數據交互是放在第二層for循環內,因此排序相同的數據冒泡執行的交換次數會大于或等于選擇排序。但是通過仔細分析時間復雜度可以得出,無論是在最好還是最差的情況下,比較的次數都是n*(n-1)/2。所以選擇排序的時間復雜度也是O(n^2)。雖然和冒泡排序的時間復雜度相等,但簡單選擇排序在性能上要略微優于冒泡排序。
插入排序
原理解析
引自維基百科插入排序(英語:Insertion Sort)是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對于未排序數據,在已排序序列中從后向前掃描,找到相應位置并插入。插入排序在實現上,通常采用in-place排序(即只需用到O(1)的額外空間的排序),因而在從后向前掃描過程中,需要反復把已排序元素逐步向后挪位,為最新元素提供插入空間。
它的工作原理是通過構建有序序列,對于未排序數據,在已排序序列中從后向前掃描,找到相應位置并插入。
解題步驟
步驟如下:
* 從第一個元素開始,該元素可以認為已經被排序
* 取出下一個元素,在已經排序的元素序列中從后向前掃描
* 如果該元素(已排序)大于新元素,將該元素移到下一位置
* 重復步驟3,直到找到已排序的元素小于或者等于新元素的位置
* 將新元素插入到該位置后
* 重復步驟2~5
我們需要整理一個無序數組為[8,3,5,4,6]。
取出第一個數字8,得到新的數組[8]。無序數組變為[3,5,4,6]。
取出第二個數字3,插入新的數組里,3比8小,得到[3,8]。無序數組變為[5,4,6]。
取出第三個數字5,插入新的數組里,5比3大,比8小,得到[3,5,8]。無序數組變為[4,6]。
取出第四個數字4,插入新的數組里,4比3大,比5小,得到[3,4,5,8]。無序數組變為[6]。
最后取出6,插入新數組里,6比5大,比8小,得到[3,4,5,6,8]。排序完成。
我們可以將需要交換位置的數字直接向右移動,然后將新的數字直接復制到正確的位置:
[ 3,5,8,4|6 ] 記住4
*
[ 3,5,8,8|6 ] 將8轉移到右側
-->
[ 3,5,5,8|6 ] 將5轉移到右側
-->
[ 3,4,5,8|6 ] 將4復制粘貼到新的位置
*
實現代碼
func insertSort() {//插入排序
var list = [61,5,33,44,22]
var nlist = [list[0]]//建立一個空數,符合條件的插入,沒插入的尾后添加
for i in 1..<list.count {
var max: Int? = nil
for j in 0..<nlist.count {
if list[i] > nlist[j] {
max = i
nlist.insert(list[i], at: j)
break
}
}
if max == nil {
nlist.append(list[i])
}
}
print(nlist)
}
func insertSortOne() {//插入排序 通過交換
var list = [61,5,33,44,22]
for i in 1..<list.count {
var y = i//從i往前找,符合條件交換
while y>0 && list[y] > list[y-1] {
let temp = list[y]
list[y] = list[y-1]
list[y-1] = temp
y -= 1
}
}
print(list)
}
func insertSortTwo() {//插入排序 通過移動
var list = [61,5,33,44,22]
for i in 1..<list.count {
var y = i//從i往前找,符合條件移動
let temp = list[y]
while y>0 && temp > list[y-1] {
list[y] = list[y-1]
y -= 1
}
list[y] = temp//找到y賦值
}
print(list)
}
最好的情況下,完全沒有任何數據移動,時間復雜度是O(n)。最壞的情況下,比較的次數為 (n+2) * (n-1)/2,移動的次數最大值為(n+4) * (n-1)/2。如果按照序列的好壞程度是隨機的,且概率都是相同的,則平均比較次數以及平均移動次數都約為 n^2/4次,所以時間復雜度為O(n ^ 2)。通過和冒泡以及簡單選擇排序對比,不難看出直接插入排序的性能稍微比前兩者好上一些。
對于插值排序算法來說,O(n^2)是它最差和平均性能表現。因為它的函數里含有兩個循環。其它類型的排序算法,比如快速排序和歸并排序,在輸入數據量很大的情況下,可以達到O(nlogn)的效果。
插值排序對于小數據量的排序來說非常快。在一些標準程序庫里,如果數據大小不大于10,它們會用插值排序來取代快速排序。
快速排序
原理解析
引自維基百科快速排序(英語:Quicksort),又稱劃分交換排序(partition-exchange sort),簡稱快排,一種排序算法,最早由東尼·霍爾提出。在平均狀況下,排序n個項目要O(nlog n)次比較。在最壞狀況下則需要 O(n^2)次比較,但這種狀況并不常見。事實上,快速排序O(nlog n)通常明顯比其他算法更快,因為它的內部循環(inner loop)可以在大部分的架構上很有效率地達成。
快速排序(Quicksort)是對冒泡排序的一種改進。它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然后再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
解題步驟
步驟:
* 從數列中挑出一個元素,稱為 “基準”(pivot),
* 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的后面(相同的數可以到任一邊)。在這個分區退出之后,該基準就處于數列的中間位置。這個稱為分區(partition)操作。
* 遞歸地(recursive)把小于基準值元素的子數列和大于基準值元素的子數列排序。
實現從大到小排序,快排的思想就是從左向右查找,比基準小的交換到右邊區域,從右向左查找,比基準大的交換到左邊區域。
也就是找到一個pivot,從左向右查找,如果比基準大的,繼續查找,比基準小的,記錄當前位置,然后從右向左查找,如果比基準小的,繼續查找,比基準大的,記錄當前位置,然后和左邊的記錄進行交換,再把基準數和中間數進行交換,保證基準在中間,兩邊分的區大于或小于基準。查找結束以左邊等于右邊的查找位置結束,然后繼續以上步驟繼續分區查找。
根據下圖理解步驟
實現代碼
func quickSort(list: inout [Int], left: Int, right: Int) {
if left > right {//左邊往右邊移,右邊往左邊移動,最后過了就停止
return
}
var i, j, pivot: Int
i = left
j = right
pivot = list[left]
while i != j {
while list[j] <= pivot && i < j {//右邊大的往左移動
j -= 1
}
while list[i] >= pivot && i < j {//左邊小的往右移動
i += 1
}
if i < j {//找到兩個對方區域的值進行交換
let temp = list[i]
list[i] = list[j]
list[j] = temp
}
}
list[left] = list[i]//此時i和j相等,處于中間位置,替換pivot值
list[i] = pivot
//重復以上動作
quickSort(list: &list, left: left, right: i-1)
quickSort(list: &list, left: i+1, right: right)
}
快排的時間復雜度為時間復雜度 O(n log n)。最差情況,遞歸調用 n 次,即空間復雜度為 O(n)。最好情況,遞歸調用 log n 次,空間復雜度為 O(log n),空間復雜度一般看最差情況,時間可以平均,但空間一定得滿足,所以空間復雜度為 O(n)。
當待排序元素類似[6,1,3,7,3]且基準元素為6時,經過分區,形成[1,3,3,6,7],兩個3的相對位置發生了改變,所是快速排序是一種不穩定排序。
堆排序
原理解析
引自維基百科堆排序(英語:Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,并同時滿足堆積的性質:即子結點的鍵值或索引總是小于(或者大于)它的父節點。
堆排序(Heap Sort)是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似完全二叉樹的結構,并同時滿足堆性質:即子結點的鍵值或索引總是小于(或者大于)它的父節點。
解題步驟
步驟:
* 最大堆調整(Max_Heapify):將堆的末端子節點作調整,使得子節點永遠小于父節點
* 創建最大堆(Build_Max_Heap):將堆所有數據重新排序
* 堆排序(HeapSort):移除位在第一個數據的根節點,并做最大堆調整的遞歸運算
通常堆是通過一維數組來實現的。在數組起始位置為0的情形中:
* 父節點 i 的左子節點在位置 (2 * i + 1);
* 父節點 i 的右子節點在位置 (2 * i + 2);
* 子節點 i 的父節點在位置 floor((i - 1) / 2);
floor 函數的作用是向下取整,所以左子節點右子節點都能通過這個公式找到正確的父節點。
最大堆調整(MAX‐HEAPIFY)的作用是保持最大堆的性質,是創建最大堆的核心子程序,作用過程如圖所示:
調整大頂堆的公式要準守任意一個節點i可以實現:i>=2i+1,i>=2i+2。也就是父節點大于等于左右兩個子節點。
所以從小到大的排序思路是:把一堆數字調整成大頂堆-->堆頂元素和末尾元素交換-->去掉末尾元素,繼續大頂堆調整-->重復以上動作
創建最大堆(Build-Max-Heap)的作用是將一個數組改造成一個最大堆,接受數組和堆大小兩個參數,Build-Max-Heap 將自下而上的調用 Max-Heapify 來改造數組,建立最大堆。因為 Max-Heapify 能夠保證下標 i 的結點之后結點都滿足最大堆的性質,所以自下而上的調用 Max-Heapify 能夠在改造過程中保持這一性質。如果最大堆的數量元素是 n,那么 Build-Max-Heap 從 Parent(n) 開始,往上依次調用 Max-Heapify。流程如下:
實現代碼
func heapSort(arr:inout Array<Int>) {
//1.構建大頂堆
for i in (0...(arr.count/2-1)).reversed(){//從二叉樹的一邊的最后一個節點開始
//從第一個非葉子結點從下至上,從右至左調整結構
adjustHeap(arr: &arr, i: i, length: arr.count)
}
//2.調整堆結構+交換堆頂元素與末尾元素
for j in (1...(arr.count-1)).reversed(){
arr.swapAt(0, j)//將堆頂元素與末尾元素進行交換
adjustHeap(arr: &arr, i: 0, length: j)//重新對堆進行調整
}
}
/**
* 調整大頂堆(僅是調整過程,建立在大頂堆已構建的基礎上)
*/
func adjustHeap(arr:inout Array<Int>,i:Int,length:Int) {
var i = i;
let temp = arr[i];//先取出當前元素i
var k=2*i+1
while k<length {//從i結點的左子結點開始,也就是2i+1處開始
if(k+1<length && arr[k]<arr[k+1]){//如果左子結點小于右子結點,k指向右子結點
k+=1;
}
if(arr[k] > temp){//如果子節點大于父節點,將子節點值賦給父節點(不用進行交換)
arr[i] = arr[k];
i = k;//記錄當前節點
}else{
break;
}
k=k*2+1//下一個節點
}
arr[i] = temp;//將temp值放到最終的位置
}
上述代碼中總過分為兩個for循環。第一個for循環就是將現在的待排序序列構建為一個最大堆,也就是maxHeap()函數的任務。第二個for循環就是逐步將每個最大值的根節點和末尾元素進行交換,然后再調整為最大堆。
在構建堆的過程中,由于是是完全二叉樹從最下層最右邊非終端節點開始構建,將它與子節點進行比較,對于每個非終端節點而言,最多進行兩次比較和交換操作,因此構建堆的時間復雜度為O(n)。在整個排序過程中,第 i 次去堆頂記錄重建堆需要時間為logi ,并且需要取 n - 1次堆記錄,所以重建對的時間復雜度為O(nlogn)。所以對的時間復雜度為O(nlogn)。
空間上只需一個暫存單元。由于記錄的比較是跳躍進行的,所以堆排序是一種不穩定的排序。最后要提醒一點,由于初始構建堆的所需的比較次數比較多。所以,一般不適合排序個數較少的數組。
希爾排序
原理解析
希爾排序是基于插入排序的以下兩點性質而提出改進方法的:
- 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
- 但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位
與插入排序通過對比相鄰的兩個元素的大小并在必要時候交換位置,希爾排序是通過比較相隔很遠的兩個元素。
兩個元素之間的距離稱為間隔。如果兩個元素在比較之后需要交換位置,則直接更換彼此的位置。這個過程減少了插值排序中很多不必要的中間復制過程,即從兩個元素更換位置前需要不斷交換相鄰元素的位置直到目的位置。
這里的最主要的思想就是,元素通過每次移動較大間隔,整個數組可以快速形成局部排序好的情況。這個會讓接下來的交換變得更加快速。因為元素之間不需要進行過多次的位置交換。
一旦某一距離長度的間隔比值交換完成,間隔會變得越來越小,然后進行相應間隔的比值交換,這樣的過程不斷重復,直到間隔為1,也就是與插值排序同樣過程的情況。然而,在希爾排序中,由于大部分數據在此時已經整理完畢,所以最后間隔為1的比值交換速度非常快。
解題步驟
步驟如下:
* 希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;
* 隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。
以n=10的一個數組49, 38, 65, 97, 26, 13, 27, 49, 55, 4為例
第一次增量 gap = 10 / 2 = 5
49 38 65 97 26 13 27 49 55 4
1A 1B
2A 2B
3A 3B
4A 4B
5A 5B
1A,1B,2A,2B等為分組標記,數字相同的表示在同一組,大寫字母表示是該組的第幾個元素, 每次對同一組的數據進行直接插入排序。即分成了五組(49, 13) (38, 27) (65, 49) (97, 55) (26, 4)這樣每組排序后就變成了(13, 49) (27, 38) (49, 65) (55, 97) (4, 26),下同。
第二次增量 gap = 5 / 2 = 2
13 27 49 55 4 49 38 65 97 26
1A 1B 1C 1D 1E
2A 2B 2C 2D 2E
第三次增量 gap = 2 / 2 = 1
4 26 13 27 38 49 49 55 97 65
1A 1B 1C 1D 1E 1F 1G 1H 1I 1J
第四次增量 gap = 1 / 2 = 0 排序完成得到數組:
4 13 26 27 38 49 49 55 65 97
實現代碼
func shellSort(arr: inout [Int]) {//希爾排序
var j: Int
var gap = arr.count / 2//獲取增量
while gap > 0 {
for i in 0 ..< gap {
j = i + gap
while j < arr.count {
if arr[j] < arr[j - gap] {
let temp = arr[j]
var k = j - gap
while (k >= 0 && arr[k] > temp) {//插入排序
arr[k + gap] = arr[k]
k -= gap
}
arr[k + gap] = temp
}
j += gap
}
}
gap /= 2//增量減半
}
}
希爾排序時間復雜度
希爾排序的時間復雜度與增量(即,步長gap)的選取有關。例如,當增量為1時,希爾排序退化成了直接插入排序,此時的時間復雜度為O(N2),而Hibbard增量的希爾排序的時間復雜度為O(N3/2)。希爾排序穩定性
希爾排序是不穩定的算法,它滿足穩定算法的定義。對于相同的兩個數,可能由于分在不同的組中而導致它們的順序發生變化。
算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。則這個排序算法是穩定的!