本文是對 Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Club是 raywenderlich.com網(wǎng)站出品的用Swift實現(xiàn)算法和數(shù)據(jù)結(jié)構(gòu)的開源項目,目前在GitHub上有18000+??,我初略統(tǒng)計了一下,大概有一百左右個的算法和數(shù)據(jù)結(jié)構(gòu),基本上常見的都包含了,是iOSer學(xué)習(xí)算法和數(shù)據(jù)結(jié)構(gòu)不錯的資源。
??andyRon/swift-algorithm-club-cn是我對Swift Algorithm Club,邊學(xué)習(xí)邊翻譯的項目。由于能力有限,如發(fā)現(xiàn)錯誤或翻譯不妥,請指正,歡迎pull request。也歡迎有興趣、有時間的小伙伴一起參與翻譯和學(xué)習(xí)??。當(dāng)然也歡迎加??,??????????。
本文的翻譯原文和代碼可以查看??swift-algorithm-club-cn/Merge Sort
這個主題已經(jīng)有輔導(dǎo)文章
目標(biāo):將數(shù)組從低到高(或從高到低)排序
歸并排序是1945年由John von Neumann發(fā)明的,是一種有效的算法,最佳、最差和平均時間復(fù)雜度都是O(n log n)。
歸并排序算法使用分而治之方法,即將一個大問題分解為較小的問題并解決它們。 歸并排序算法可分為 先拆分 和 后合并。
假設(shè)您需要按正確的順序?qū)﹂L度為 n 的數(shù)組進(jìn)行排序。 歸并排序算法的工作原理如下:
- 將數(shù)字放在未排序的堆中。
- 將堆分成兩部分。 那么現(xiàn)在就有兩個未排序的數(shù)字堆。
- 繼續(xù)分裂兩個未排序的數(shù)字堆,直到你不能分裂為止。 最后,你將擁有 n 個堆,每堆中有一個數(shù)字。
- 通過順序配對,開始 合并 堆。 在每次合并期間,將內(nèi)容按排序順序排列。 這很容易,因為每個單獨(dú)的堆已經(jīng)排序(譯注:單個數(shù)字沒有所謂的順序,就是排好序的)。
例子
拆分
假設(shè)給你一個長度為n的未排序數(shù)組:[2,1,5,4,9]
。 目標(biāo)是不斷拆分堆,直到你不能拆分為止。
首先,將數(shù)組分成兩半:[2,1]
和[5,4,9]
。 你能繼續(xù)拆分嗎? 是的你可以!
專注于左邊堆。 將[2,1]
拆分為[2]
和[1]
。 你能繼續(xù)拆分嗎? 不能了。檢查右邊的堆。
將[5,4,9]
拆分為[5]
和[4,9]
。 不出所料,[5]
不能再拆分了,但是[4,9]
可以分成[4]
和[9]
。
拆分最終結(jié)果為:[2]``[1]``[5]``[4]``[9]
。 請注意,每個堆只包含一個元素。
合并
您已經(jīng)拆分了數(shù)組,您現(xiàn)在應(yīng)該 合并并排序 拆分后的堆。 請記住,這個想法是解決許多小問題而不是一個大問題。 對于每次合并迭代,您必須關(guān)注將一堆與另一堆合并。
對于堆 [2]
[1]
[5]
[4]
[9]
,第一次合并的結(jié)果是[1,2]
和[4,5]
和[9]
。 由于[9]
的位置落單,所以在合并過程中沒有堆與之合并了。
下一次將合并[1,2]
和[4,5]
。 結(jié)果[1,2,4,5]
,再次由于[9]
的位置落單不需要合并。
只剩下兩堆[1,2,4,5]
和[9]
,合并后完成排序的數(shù)組為[1,2,4,5,9]
。
自上而下的實施(遞歸法)
歸并排序的Swift實現(xiàn):
func mergeSort(_ array: [Int]) -> [Int] {
guard array.count > 1 else { return array } // 1
let middleIndex = array.count / 2 // 2
let leftArray = mergeSort(Array(array[0..<middleIndex])) // 3
let rightArray = mergeSort(Array(array[middleIndex..<array.count])) // 4
return merge(leftPile: leftArray, rightPile: rightArray) // 5
}
代碼的逐步說明:
如果數(shù)組為空或包含單個元素,則無法將其拆分為更小的部分,返回數(shù)組就行。
找到中間索引。
使用上一步中的中間索引,遞歸地分割數(shù)組的左側(cè)。
此外,遞歸地分割數(shù)組的右側(cè)。
最后,將所有值合并在一起,確保它始終排序。
這兒是合并的算法:
func merge(leftPile: [Int], rightPile: [Int]) -> [Int] {
// 1
var leftIndex = 0
var rightIndex = 0
// 2
var orderedPile = [Int]()
// 3
while leftIndex < leftPile.count && rightIndex < rightPile.count {
if leftPile[leftIndex] < rightPile[rightIndex] {
orderedPile.append(leftPile[leftIndex])
leftIndex += 1
} else if leftPile[leftIndex] > rightPile[rightIndex] {
orderedPile.append(rightPile[rightIndex])
rightIndex += 1
} else {
orderedPile.append(leftPile[leftIndex])
leftIndex += 1
orderedPile.append(rightPile[rightIndex])
rightIndex += 1
}
}
// 4
while leftIndex < leftPile.count {
orderedPile.append(leftPile[leftIndex])
leftIndex += 1
}
while rightIndex < rightPile.count {
orderedPile.append(rightPile[rightIndex])
rightIndex += 1
}
return orderedPile
}
這種方法可能看起來很可怕,但它非常簡單:
在合并時,您需要兩個索引來跟蹤兩個數(shù)組的進(jìn)度。
這是合并后的數(shù)組。 它現(xiàn)在是空的,但是你將在下面的步驟中通過添加其他數(shù)組中的元素構(gòu)建它。
這個while循環(huán)將比較左側(cè)和右側(cè)的元素,并將它們添加到
orderedPile
,同時確保結(jié)果保持有序。如果前一個while循環(huán)完成,則意味著
leftPile
或rightPile
中的一個的內(nèi)容已經(jīng)完全合并到orderedPile
中。此時,您不再需要進(jìn)行比較。只需依次添加剩下一個數(shù)組的其余內(nèi)容到orderedPile
。
merge()
函數(shù)如何工作的例子。假設(shè)我們有以兩個個堆:leftPile = [1,7,8]
和rightPile = [3,6,9]
。 請注意,這兩個堆都已單獨(dú)排序 -- 合并排序總是如此的。 下面的步驟就將它們合并為一個更大的排好序的堆:
leftPile rightPile orderedPile
[ 1, 7, 8 ] [ 3, 6, 9 ] [ ]
l r
左側(cè)索引(此處表示為l
)指向左側(cè)堆的第一個項目1
。 右則索引r
指向3
。 因此,我們添加到orderedPile
的第一項是1
。 我們還將左側(cè)索引l
移動到下一個項。
leftPile rightPile orderedPile
[ 1, 7, 8 ] [ 3, 6, 9 ] [ 1 ]
-->l r
現(xiàn)在l
指向7
但是r
仍然處于3
。 我們將最小的項3
添加到有序堆中。 現(xiàn)在的情況是:
leftPile rightPile orderedPile
[ 1, 7, 8 ] [ 3, 6, 9 ] [ 1, 3 ]
l -->r
重復(fù)上面的過程。 在每一步中,我們從leftPile
或rightPile
中選擇最小的項,并將該項添加到orderedPile
中:
leftPile rightPile orderedPile
[ 1, 7, 8 ] [ 3, 6, 9 ] [ 1, 3, 6 ]
l -->r
leftPile rightPile orderedPile
[ 1, 7, 8 ] [ 3, 6, 9 ] [ 1, 3, 6, 7 ]
-->l r
leftPile rightPile orderedPile
[ 1, 7, 8 ] [ 3, 6, 9 ] [ 1, 3, 6, 7, 8 ]
-->l r
現(xiàn)在,左堆中沒有更多物品了。 我們只需從右邊的堆中添加剩余的項目,我們就完成了。 合并的堆是[1,3,6,7,8,9]
。
請注意,此算法非常簡單:它從左向右移動通過兩個堆,并在每個步驟選擇最小的項目。 這是有效的,因為我們保證每個堆都已經(jīng)排序。
譯注: 關(guān)于自上而下的執(zhí)行(遞歸法)的歸并排序,我找了一個比較形象的動圖,來源
遞歸的歸并排序
自下而上的實施(迭代)
到目前為止你看到的合并排序算法的實現(xiàn)被稱為“自上而下”的方法,因為它首先將數(shù)組拆分成更小的堆然后合并它們。排序數(shù)組(而不是鏈表)時,實際上可以跳過拆分步驟并立即開始合并各個數(shù)組元素。 這被稱為“自下而上”的方法。
下面是Swift中一個完整的自下而上的實現(xiàn):
func mergeSortBottomUp<T>(_ a: [T], _ isOrderedBefore: (T, T) -> Bool) -> [T] {
let n = a.count
var z = [a, a] // 1
var d = 0
var width = 1
while width < n { // 2
var i = 0
while i < n { // 3
var j = i
var l = i
var r = i + width
let lmax = min(l + width, n)
let rmax = min(r + width, n)
while l < lmax && r < rmax { // 4
if isOrderedBefore(z[d][l], z[d][r]) {
z[1 - d][j] = z[d][l]
l += 1
} else {
z[1 - d][j] = z[d][r]
r += 1
}
j += 1
}
while l < lmax {
z[1 - d][j] = z[d][l]
j += 1
l += 1
}
while r < rmax {
z[1 - d][j] = z[d][r]
j += 1
r += 1
}
i += width*2
}
width *= 2
d = 1 - d // 5
}
return z[d]
}
它看起來比自上而下的版本更令人生畏,但請注意主體包含與merge()
相同的三個while
循環(huán)。
值得注意的要點(diǎn):
歸并排序算法需要一個臨時工作數(shù)組,因為你不能合并左右堆并同時覆蓋它們的內(nèi)容。 因為為每個合并分配一個新數(shù)組是浪費(fèi),我們使用兩個工作數(shù)組,我們將使用
d
的值在它們之間切換,它是0或1。數(shù)組z[d]
用于讀,z[1 - d]
用于寫。 這稱為 雙緩沖。從概念上講,自下而上版本的工作方式與自上而下版本相同。首先,它合并每個元素的小堆,然后它合并每個堆兩個元素,然后每個堆成四個元素,依此類推。堆的大小由
width
給出。 最初,width
是1
但是在每次循環(huán)迭代結(jié)束時,我們將它乘以2,所以這個外循環(huán)確定要合并的堆的大小,并且要合并的子數(shù)組在每一步中變得更大。內(nèi)循環(huán)穿過堆并將每對堆合并成一個較大的堆。 結(jié)果寫在
z[1 - d]
給出的數(shù)組中。這與自上而下版本的邏輯相同。 主要區(qū)別在于我們使用雙緩沖,因此從
z[d]
讀取值并寫入z [1 - d]
。它還使用isOrderedBefore
函數(shù)來比較元素而不僅僅是<
,因此這種合并排序算法是通用的,您可以使用它來對任何類型的對象進(jìn)行排序。此時,數(shù)組
z[d]
的大小width
的堆已經(jīng)合并為數(shù)組z[1-d]
中更大的大小width * 2
。在這里,我們交換活動數(shù)組,以便在下一步中我們將從我們剛剛創(chuàng)建的新堆中讀取。
這個函數(shù)是通用的,所以你可以使用它來對你想要的任何類型對象進(jìn)行排序,只要你提供一個正確的isOrderedBefore
閉包來比較元素。
怎么使用它的示例:
let array = [2, 1, 5, 4, 9]
mergeSortBottomUp(array, <) // [1, 2, 4, 5, 9]
譯注:關(guān)于迭代的歸并排序,我找到一個圖來表示,來源
迭代的歸并排序
性能
歸并排序算法的速度取決于它需要排序的數(shù)組的大小。 數(shù)組越大,它需要做的工作就越多。
初始數(shù)組是否已經(jīng)排序不會影響歸并排序算法的速度,因為無論元素的初始順序如何,您都將進(jìn)行相同數(shù)量的拆分和比較。
因此,最佳,最差和平均情況的時間復(fù)雜度將始終為 O(n log n)。
歸并排序算法的一個缺點(diǎn)是它需要一個臨時的“工作”數(shù)組,其大小與被排序的數(shù)組相同。 它不是原地排序,不像例如quicksort。
大多數(shù)實現(xiàn)歸并排序算法是穩(wěn)定的排序。這意味著具有相同排序鍵的數(shù)組元素在排序后將保持相對于彼此的相同順序。這對于數(shù)字或字符串等簡單值并不重要,但在排序更復(fù)雜的對象時,如果不是穩(wěn)定的排序可能會出現(xiàn)問題。
譯注:當(dāng)元素相同時,排序后依然保持排序之前的相對順序,那么這個排序算法就是穩(wěn)定的。穩(wěn)定的排序有:插入排序、計數(shù)排序、歸并排序、基數(shù)排序等等,詳見穩(wěn)定的排序。
擴(kuò)展閱讀
作者:Kelvin Lau. Additions , Matthijs Hollemans
翻譯:Andy Ron
校對:Andy Ron