【譯】Swift算法俱樂部-歸并排序

Swift算法俱樂部

本文是對 Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Clubraywenderlich.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
}

代碼的逐步說明:

  1. 如果數(shù)組為空或包含單個元素,則無法將其拆分為更小的部分,返回數(shù)組就行。

  2. 找到中間索引。

  3. 使用上一步中的中間索引,遞歸地分割數(shù)組的左側(cè)。

  4. 此外,遞歸地分割數(shù)組的右側(cè)。

  5. 最后,將所有值合并在一起,確保它始終排序。

這兒是合并的算法:

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
}

這種方法可能看起來很可怕,但它非常簡單:

  1. 在合并時,您需要兩個索引來跟蹤兩個數(shù)組的進(jìn)度。

  2. 這是合并后的數(shù)組。 它現(xiàn)在是空的,但是你將在下面的步驟中通過添加其他數(shù)組中的元素構(gòu)建它。

  3. 這個while循環(huán)將比較左側(cè)和右側(cè)的元素,并將它們添加到orderedPile,同時確保結(jié)果保持有序。

  4. 如果前一個while循環(huán)完成,則意味著leftPilerightPile中的一個的內(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ù)上面的過程。 在每一步中,我們從leftPilerightPile中選擇最小的項,并將該項添加到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):

  1. 歸并排序算法需要一個臨時工作數(shù)組,因為你不能合并左右堆并同時覆蓋它們的內(nèi)容。 因為為每個合并分配一個新數(shù)組是浪費(fèi),我們使用兩個工作數(shù)組,我們將使用d的值在它們之間切換,它是0或1。數(shù)組z[d]用于讀,z[1 - d]用于寫。 這稱為 雙緩沖。

  2. 從概念上講,自下而上版本的工作方式與自上而下版本相同。首先,它合并每個元素的小堆,然后它合并每個堆兩個元素,然后每個堆成四個元素,依此類推。堆的大小由width給出。 最初,width1但是在每次循環(huán)迭代結(jié)束時,我們將它乘以2,所以這個外循環(huán)確定要合并的堆的大小,并且要合并的子數(shù)組在每一步中變得更大。

  3. 內(nèi)循環(huán)穿過堆并將每對堆合并成一個較大的堆。 結(jié)果寫在z[1 - d]給出的數(shù)組中。

  4. 這與自上而下版本的邏輯相同。 主要區(qū)別在于我們使用雙緩沖,因此從z[d]讀取值并寫入z [1 - d]。它還使用isOrderedBefore函數(shù)來比較元素而不僅僅是<,因此這種合并排序算法是通用的,您可以使用它來對任何類型的對象進(jìn)行排序。

  5. 此時,數(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明AGI閱讀 16,009評論 3 119
  • 【童話三十題】 1、精靈的篝火晚會 2、騎士和巨龍 3、帽子茶會 4、于穹頂下禱告 5、牧場上的黃水仙 6、知更鳥...
    個人日常腦洞堆積地閱讀 1,168評論 0 2
  • 題目大概意思是給一個 [n - 1] * [n] 的矩陣, 輸出一行 n 個數(shù)分別為原矩陣去掉第 i 行之后的子矩...
    evilgiven閱讀 484評論 4 0