使用 Swift 實現歸併排序

知識小集發表於2019-02-20

原文連結

| 作者:Jimmy M Andersson

| 連結:medium.com/swlh/sortin…

之前介紹了堆排序,這是一種基於堆的排序演算法。今天,我們進一步深入研究排序演算法,來看看歸併排序,這是一種時間複雜度為 O(n * log(n)) 的排序演算法,作為權衡,其空間複雜度為 O(n)

本文的具體實現可以檢視相應的 XCode Playground 檔案

什麼是歸併排序?

歸併排序屬性分治法一類的演算法。分治法基本思想是解決只包含一半資料的問題要容易得多,因此可以嘗試將資料遞迴地分割成兩個大小相等的資料集來處理。

在歸併排序中,對原始陣列大小一半的資料進行排序要容易得多。將原始陣列分割成四個四分之一大小的陣列分別排序會更簡單,因此我們執行遞迴分割操作。這是該演算法巧妙的地方。由於我們處理的是有限大小的陣列,所以最終分割得到的所有陣列大小都會少於兩個元素,而這樣的陣列可以看作是已排序的。

當到達上面所說的狀態時,遞迴終止,我們開始展開呼叫堆疊。在這個過程中,我們通過比較每個陣列中的元素,並將它們插入到一個新陣列中來合併陣列,然後將這些陣列返回給呼叫堆疊的下一幀。來看看下面的草圖,瞭解它的工作原理:

使用 Swift 實現歸併排序

構造演算法

我們將採用面向協議的方法,這意味著我們將擴充套件包含符合 Comparable 協議的元素的陣列的 Array 實現。我們還將實現一個返回陣列的排序副本的方法,而不是替換我們呼叫方法的陣列。如下程式碼所示:

extension Array where Element: Comparable {
 public mutating func mergeSort() {
    let startSlice = self[0..<self.count]
    let slice = mergeSort(startSlice)
    let array = Array(slice)
    self = array
  }
  
  public func mergeSorted() -> Array<Element> {
    let startSlice = self[0..<self.count]
    let slice = mergeSort(startSlice)
    let array = Array(slice)
    return array
  }
}
複製程式碼

請注意,我們宣告瞭一個名為 startSlice 的變數。我們將使用一個名為 ArraySlice 的泛型結構,它將幫助我們避免在呼叫堆疊中對陣列進行不必要的複製。相反,我們將建立一個已分配記憶體的檢視,並只在我們將兩個切片合併在一起時才分配新記憶體。當對 .mergeSort(_:) 的呼叫返回時,我們需要將它轉換為一個 Array,以便與我們的返回型別相容。但是,請注意,Swift 編譯器是一個非常智慧的構造,它只是讓新的 Array 物件使用 ArraySlice 中已經分配的記憶體,因此不需要任何開銷。

接下來,我們定義了 .mergeSort(_:) 方法,它仍然在擴充套件範圍內:

private func mergeSort(_ array: ArraySlice<Element>) -> ArraySlice<Element> {
    if array.count < 2 {
      return array
    } else {
      let midIndex = (array.endIndex + array.startIndex) / 2
      let slice1 = mergeSort(array[array.startIndex..<midIndex])
      let slice2 = mergeSort(array[midIndex..<array.endIndex])
      return merge(slice1, slice2)
    }
  }
複製程式碼

這裡包含我們決定是否可以將陣列的分割部分視為“已排序”的部分。如果陣列的元素少於兩個元素,則按定義是已排序的,因此我們只返回相同的陣列。如果它包含更多元素,我們通過建立兩個新的 ArraySlice 物件並將它們傳遞給遞迴呼叫再次拆分它。當兩個遞迴呼叫返回時,我們可以確定我們有兩個排序的陣列切片可以使用,所以我們呼叫方法 merge(_:_:) 將兩個切片合併為一個並返回它。 merge(_:_:) 看起來像這樣:

private func merge(_ firstArray: ArraySlice<Element>, _ secondArray: ArraySlice<Element>) -> ArraySlice<Element> {
    var newArray = ArraySlice<Element>()
    newArray.reserveCapacity(firstArray.count + secondArray.count)
    var index1 = firstArray.startIndex
    var index2 = secondArray.startIndex
    
    while index1 < firstArray.endIndex && index2 < secondArray.endIndex {
      if firstArray[index1] < secondArray[index2] {
        newArray.append(firstArray[index1])
        index1 += 1
      } else {
        newArray.append(secondArray[index2])
        index2 += 1
      }
    }
    
    if index1 < firstArray.endIndex {
      let range = index1..<firstArray.endIndex
      let remainingElements = firstArray[range]
      newArray.append(contentsOf: remainingElements)
    }
    if index2 < secondArray.endIndex {
      let range = index2..<secondArray.endIndex
      let remainingElements = secondArray[range]
      newArray.append(contentsOf: remainingElements)
    }
    
    return newArray
  }
複製程式碼

這個有點棘手,所以請花時間閱讀並理解它。我們首先建立一個新的 ArraySlice 物件和兩個變數,以跟蹤我們當前在每個切片的索引。然後我們比較每個切片的元素,將最小的元素放入我們的新 ArraySlice 物件中,直到完成其中一個原始切片。最後,我們檢查是否還有剩餘的元素需要插入。一旦完成,我們將新切片返回到呼叫函式,我們就完成了排序操作。

關注我們

歡迎關注我們的公眾號:iOS-Tips,也歡迎加入我們的群組討論問題。可以加微信 coldlight_hh/wsy9871 進入我們的 iOS/flutter 微信群。

使用 Swift 實現歸併排序

相關文章