前言
關於陣列排序的問題,在之前的文章有很詳細的介紹(連結:《iOS面試之道》演算法基礎學習(下))。在這篇文章中,筆者會對經典的冒泡演算法進行優化。先來看下未經優化的冒泡演算法:
冒泡演算法
//經典版
func bubbleSort( _ array: inout [Int]) -> [Int]? {
//i表示陣列中依次比較的總次數
for i in 0..<array.count {
for j in 0..<array.count - i - 1) {
//j表示下標,判斷第j位和j+1位元素大小,進行排序
if array[j] > array[j+1] {
//元素左右進行交換
let tmp = array[j]
array[j] = array[j+1]
array[j+1] = tmp
}
}
}
return array
}
複製程式碼
我們都知道傳統的冒泡演算法是通過2個for迴圈來比較相鄰元素的值,每次一輪大的迴圈結束,都會固定一個值,然後對已固定數值外的元素接著遍歷,最終使得陣列有序。這樣的寫法看上去是沒有問題,但是時間複雜度卻非常高達到了O(n^2),所以說這個演算法不是一個“優秀”的演算法。
解決方法
怎麼才能優化這個演算法呢,我們還是通過個例子來看下。假設現在有一個陣列array = [7,6,1,2,3,4],不難看出這個陣列其實經歷過2次迴圈就已經使得陣列有序了,但是程式還是會繼續進行無意義的比較。那麼這時我們可以通過一個Bool值,來確定當前陣列是否已經有序,若有序則中斷迴圈,直接看程式碼:
//進化版
func bubbleSort( _ array: inout [Int]) -> [Int]? {
for i in 0..<array.count {
//設定bool判斷是否有序
var isSorted = true
for j in 0..<array.count - (i+1) {
if array[j] > array[j+1] {
let tmp = array[j]
array[j] = array[j+1]
array[j+1] = tmp
//有交換 陣列依舊無序
isSorted = false
}
}
if(isSorted) {
//有序 中斷迴圈
break
}
}
return array
}
複製程式碼
解決方法簡單粗暴,每次外層迴圈開始isSorted為true,若元素未進行位置交換,則證明陣列已有序,結束迴圈。
那這個演算法還有繼續優化的空間麼,答案是肯定的。用另一個例子來解釋一下,假設現在陣列array = [1,3,2,4,5,6]。按照上面的寫法我們的確可以在3輪判斷後就結束迴圈,非常高效。但問題是後面的4,5,6元素已經有序,每次的迴圈還是會對後三個元素進行判斷。
解決方法
問題的所在是因為我們對已排序好的陣列進行了很多無意義的判斷,所以需要我們對已經排好序的元素進行邊界限定,來減少判斷,看下程式碼:
//超級進化版
func bubbleSort( _ array: inout [Int]) -> [Int]? {
//記錄陣列邊界
var arrayBorder = array.count - 1
for i in 0..<array.count {
//設定bool判斷是否有序
var isSorted = true
for j in 0..<arrayBorder {
if array[j] > array[j+1] {
let tmp = array[j]
array[j] = array[j+1]
array[j+1] = tmp
//有交換 陣列依舊無序
isSorted = false
//記錄最後一次交換的位置
arrayBorder = j
}
}
if(isSorted) {
//有序 中斷迴圈
break
}
}
return array
}
複製程式碼
如果說上面進化版的方法是減少外層i迴圈的無用判斷,那麼新增陣列邊界,就是減少內部j迴圈的無用判斷。通過記錄需要交換位置的邊界值,來避免不必要的判斷。
經過了2個版本的進化,氣泡排序已經實現了大蛻變,減少了很多不必要的操作。但是此時的氣泡排序還不是那麼的優秀,比如陣列array = [2,3,4,5,6,1],可以看出除了元素1以外,其他元素都已經完成排序。但是把該陣列帶入上面超級進化版本,你會發現它還是進行了5輪的判斷才完成任務。
解決方法
出現上述問題的主要原因還是迴圈一直是從左至右進行,每次的起始都是從第0位開始。如果說能讓迴圈從右至左再進行一輪迴圈,就能很快的把元素1放到首位。這就要介紹一種新的排序方法--雞尾酒排序。
雞尾酒排序:也就是定向氣泡排序, 雞尾酒攪拌排序, 攪拌排序 (也可以視作選擇排序的一種變形), 漣漪排序, 來回排序 or 快樂小時排序, 是氣泡排序的一種變形。此演演算法與氣泡排序的不同處在於排序時是以雙向在序列中進行排序。
知道了解決方法,直接來看下最終的究極進化版本:
//究極進化版
func cocktailSort( _ array: inout [Int]) -> [Int]? {
//陣列左邊界
var arrayLeftBorder = 0
//陣列右邊界
var arrayRightBorder = array.count - 1
for i in 0..<array.count/2 {
//設定bool判斷是否有序
var isSorted = true
//第一輪迴圈 左->右
for j in arrayLeftBorder..<arrayRightBorder {
if array[j] > array[j+1] {
let tmp = array[j]
array[j] = array[j+1]
array[j+1] = tmp
//有交換 陣列依舊無序
isSorted = false
//記錄最後一次交換的位置
arrayRightBorder = j
}
}
if(isSorted) {
//有序 中斷迴圈
break
}
//再次初始化isSorted位true
isSorted = true
//第二輪迴圈 右->左
for j in (arrayLeftBorder+1...arrayRightBorder).reversed() {
if array[j] < array[j-1] {
let tmp = array[j]
array[j] = array[j-1]
array[j-1] = tmp
//有交換 陣列依舊無序
isSorted = false
//記錄最後一次交換的位置
arrayLeftBorder = j
}
}
if(isSorted) {
//有序 中斷迴圈
break
}
}
return array
}
複製程式碼
雞尾酒排序將之前完整的一輪迴圈拆分為從左->右和從右->左兩個子迴圈,這就保證了排序的雙向進行,效率較單項迴圈來說更高。 與此同時在這兩個迴圈中加入了之前兩個版本的特性isSorted和有序邊界,使得排序更加高效。
總結
隨著對氣泡排序的不斷升入理解,發現了實際排序中的問題,通過將幾種方法的組合使用,使得改進後的氣泡排序更加高效,當資料量非常巨大時效率提升非常明顯。