使用 Swift 實現堆排序
| 作者:Jimmy M Andersson
| 連結: medium.com/appcoda-tut…
| 公眾號連結: mp.weixin.qq.com/s/kfqsTnJHb…
排序是計算機的一項主要任務。這並不是因為排序本身非常有趣,而是因為很多其它演算法依賴於排序才能正常執行。本文主要描述如何實現堆排序演算法,該演算法依賴於稱為堆的資料結構。
本文的具體實現可以檢視對應的 XCode Playground 檔案 。
堆
堆是一個完整且部分排序的二叉樹。通俗一點講就是它總是將新資料節點插入最深一層的左側。從某種意義上講,元素間是有順序的,儘管實際上並未做排序操作。
在本教程中,我們將使用最大堆(Max Heap)實現一個始終在 O(n * log(n)) 時間內執行的排序演算法。首先,我們將實現一個 SortingAlgorithms
類,該類帶有一個對陣列進行排序的靜態函式,然後再使用面向協議的解決方案。
關於最大堆,有三個重要事項:
- 始終保證樹根中的元素值最大;
- 任何一個節點如果有子節點的話,它的值總是大於它所有的子節點的值;
- 堆通常可以以陣列的形式實現,其中可以使用非常簡單的數學公式來計算特定索引的父節點和子節點。這將使我們能夠實現快速有效的排序。
程式碼
首先,我們來擴充套件 Swift 標準庫的 Int 型別的實現,以抽象出獲取父節點和子節點索引的運算公式。如下程式碼所示:
private extension Int { var parent: Int { return (self - 1) / 2 } var leftChild: Int { return (self * 2) + 1 } var rightChild: Int { return (self * 2) + 2 } } 複製程式碼
使用這些程式碼,我們可以通過計算屬性來計算索引,而不是需要時直接用數學公式來計算。這樣保證了可讀性。另外這是一個私有擴充套件,意味著它不會影響 Int 的整體性,而只是在該檔案作用域中可用。
接下來,讓我們分步來說明排序演算法的各個步驟。
首先,我們將從陣列構建一個最大堆。基本操作是將新元素插入到堆的末尾再交換到正確的位置,因此我們可以使用簡單的迴圈來模擬插入操作。我們先假定陣列只有兩個元素,“堆積”這兩個元素,在迴圈的每個迭代中,我們插入一個元素並重新堆積。如下所示:

操作完成後,陣列看上去並沒有特意排序。事實上,看起來更糟糕。這是因為我們使用陣列儲存了樹的節點。看一下操作前後的對比:

看上去我們只是交換了元素,但事實上,我們剛剛建立了一個將用於完成排序的屬性。
獲取第一個元素,然後將其與最後一個元素交換,我們可以把最大的元素放到最後。然後假定陣列長度減 1,然後重新堆積這個子陣列,又可以得到這個子陣列的最大元素。然後將子陣列的最大元素放到子陣列最後,這樣依此類推,就可以得到一個完全排序的陣列。

我們的 .heapSort(_:) 方法的程式碼如下,包括構建和縮小堆。
class SortingAlgorithms { private init() {} public static func heapSort<DataType: Comparable>(_ array: inout [DataType]) { if array.count < 2 { return } buildHeap(&array) shrinkHeap(&array) } private static func buildHeap<DataType: Comparable>(_ array: inout [DataType]) { for index in 1..<array.count { var child = index var parent = child.parent while child > 0 && array[child] > array[parent] { swap(child, with: parent, in: &array) child = parent parent = child.parent } } } private static func shrinkHeap<DataType: Comparable>(_ array: inout [DataType]) { for index in stride(from: array.count - 1, to: 0, by: -1) { swap(0, with: index, in: &array) var parent = 0 var leftChild = parent.leftChild var rightChild = parent.rightChild while parent < index { var maxChild = -1 if leftChild < index { maxChild = leftChild } else { break } if rightChild < index && array[rightChild] > array[maxChild] { maxChild = rightChild } guard array[maxChild] > array[parent] else { break } swap(parent, with: maxChild, in: &array) parent = maxChild leftChild = parent.leftChild rightChild = parent.rightChild } } } private static func swap<DataType: Comparable>(_ firstIndex: Int, with secondIndex: Int, in array: inout [DataType]) { let temp = array[firstIndex] array[firstIndex] = array[secondIndex] array[secondIndex] = temp } } 複製程式碼
這就是我們想要的,不過我們可以讓它更乾淨一些。
面向協議的實現
通過擴充套件可比較元素型別的 Array 型別,我們能得到一些好處。一個是程式碼量更少,另一個是可以直接在物件上呼叫方法,而不需要如下處理:
SortingAlgorithms.heapSort(&myArray) 複製程式碼
而是這樣:
myArray.heapSort() 複製程式碼
這樣更加清晰。元素型別不符合 Comparable 協議時,編輯器甚至不會在陣列物件上智慧提示 .heapSort()。
public extension Array where Element: Comparable { public mutating func heapSort() { buildHeap() shrinkHeap() } private mutating func buildHeap() { for index in 1..<self.count { var child = index var parent = child.parent while child > 0 && self[child] > self[parent] { swapAt(child, parent) child = parent parent = child.parent } } } private mutating func shrinkHeap() { for index in stride(from: self.count - 1, to: 0, by: -1) { swapAt(0, index) var parent = 0 var leftChild = parent.leftChild var rightChild = parent.rightChild while parent < index { var maxChild = -1 if leftChild < index { maxChild = leftChild } else { break } if rightChild < index && self[rightChild] > self[maxChild] { maxChild = rightChild } guard self[maxChild] > self[parent] else { break } swapAt(parent, maxChild) parent = maxChild leftChild = parent.leftChild rightChild = parent.rightChild } } } } 複製程式碼
Done!!!