【譯】Swift算法俱樂部-第k大元素問題

本文是對 Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Clubraywenderlich.com網站出品的用Swift實現算法和數據結構的開源項目,目前在GitHub上有18000+⭐️,我初略統計了一下,大概有一百左右個的算法和數據結構,基本上常見的都包含了,是iOSer學習算法和數據結構不錯的資源。
🐙andyRon/swift-algorithm-club-cn是我對Swift Algorithm Club,邊學習邊翻譯的項目。因爲能力有限,如發現錯誤或翻譯不妥,請指正,歡迎pull request。也歡迎有興趣、有時間的小夥伴一塊兒參與翻譯和學習🤓。固然也歡迎加⭐️,🤩🤩🤩🤨🤪。
本文的翻譯原文和代碼能夠查看🐙swift-algorithm-club-cn/Kth Largest Elementgit


第k大元素問題(k-th Largest Element Problem)github

你有一個整數數組a。 編寫一個算法,在數組中找到第k大的元素。算法

例如,第1個最大元素是數組中出現的最大值。 若是數組具備n個元素,則第n最大元素是最小值。 中位數是第n/2最大元素。swift

樸素的解決方案

如下是半樸素的解決方案。 它的時間複雜度是 O(nlogn),由於它首先對數組進行排序,所以也使用額外的 O(n) 空間。數組

func kthLargest(a: [Int], k: Int) -> Int? {
  let len = a.count
  if k > 0 && k <= len {
    let sorted = a.sorted()
    return sorted[len - k]
  } else {
    return nil
  }
}
複製代碼

kthLargest() 函數有兩個參數:由整數組成的數組a,已經整數k。 它返回第k大元素。數據結構

讓咱們看一個例子並運行算法來看看它是如何工做的。 給定k = 4和數組:dom

[ 7, 92, 23, 9, -1, 0, 11, 6 ]
複製代碼

最初沒有找到第k大元素的直接方法,但在對數組進行排序以後,它很是簡單。 這是排完序的數組:函數

[ -1, 0, 6, 7, 9, 11, 23, 92 ]
複製代碼

如今,咱們所要作的就是獲取索引a.count - k的值:學習

a[a.count - k] = a[8 - 4] = a[4] = 9
複製代碼

固然,若是你正在尋找第k個最小的元素,你會使用a [k-1]網站

更快的解決方案

有一種聰明的算法結合了二分搜索快速排序的思想來達到**O(n)**解決方案。

回想一下,二分搜索會一次又一次地將數組分紅兩半,以便快速縮小您要搜索的值。 這也是咱們在這裏所作的。

快速排序還會拆分數組。它使用分區將全部較小的值移動到數組的左側,將全部較大的值移動到右側。在圍繞某個基準進行分區以後,該基準值將已經處於其最終的排序位置。 咱們能夠在這裏利用它。

如下是它的工做原理:咱們選擇一個隨機基準,圍繞該基準對數組進行分區,而後像二分搜索同樣運行,只在左側或右側分區中繼續。這一過程重複進行,直到咱們找到一個剛好位於第k位置的基準。

讓咱們再看看初始的例子。 咱們正在尋找這個數組中的第4大元素:

[ 7, 92, 23, 9, -1, 0, 11, 6 ]
複製代碼

若是咱們尋找第k個最小項,那麼算法會更容易理解,因此讓咱們採用k = 4並尋找第4個最小元素。

請注意,咱們沒必要先對數組進行排序。 咱們隨機選擇其中一個元素做爲基準,假設是11,並圍繞它分割數組。 咱們最終會獲得這樣的結論:

[ 7, 9, -1, 0, 6, 11, 92, 23 ]
 <------ smaller    larger -->
複製代碼

如您所見,全部小於11的值都在左側; 全部更大的值都在右邊。基準值11如今處於最終排完序的位置。基準的索引是5,所以第4個最小元素確定位於左側分區中的某個位置。從如今開始咱們能夠忽略數組的其他部分:

[ 7, 9, -1, 0, 6, x, x, x ]
複製代碼

再次讓咱們選擇一個隨機的樞軸,讓咱們說6,而後圍繞它劃分數組。 咱們最終會獲得這樣的結論:

[ -1, 0, 6, 9, 7, x, x, x ]
複製代碼

基準值6在索引2處結束,因此顯然第4個最小的項必須在右側分區中。 咱們能夠忽略左側分區:

[ x, x, x, 9, 7, x, x, x ]
複製代碼

咱們再次隨機選擇一個基準值,假設是9,並對數組進行分區:

[ x, x, x, 7, 9, x, x, x ]
複製代碼

基準值9的索引是4,這正是咱們正在尋找的 k。 咱們完成了! 注意這隻須要幾個步驟,咱們沒必要先對數組進行排序。

如下函數實現了這些想法:

public func randomizedSelect<T: Comparable>(_ array: [T], order k: Int) -> T {
  var a = array

  func randomPivot<T: Comparable>(_ a: inout [T], _ low: Int, _ high: Int) -> T {
    let pivotIndex = random(min: low, max: high)
    a.swapAt(pivotIndex, high)
    return a[high]
  }

  func randomizedPartition<T: Comparable>(_ a: inout [T], _ low: Int, _ high: Int) -> Int {
    let pivot = randomPivot(&a, low, high)
    var i = low
    for j in low..<high {
      if a[j] <= pivot {
        a.swapAt(i, j)
        i += 1
      }
    }
    a.swapAt(i, high)
    return i
  }

  func randomizedSelect<T: Comparable>(_ a: inout [T], _ low: Int, _ high: Int, _ k: Int) -> T {
    if low < high {
      let p = randomizedPartition(&a, low, high)
      if k == p {
        return a[p]
      } else if k < p {
        return randomizedSelect(&a, low, p - 1, k)
      } else {
        return randomizedSelect(&a, p + 1, high, k)
      }
    } else {
      return a[low]
    }
  }

  precondition(a.count > 0)
  return randomizedSelect(&a, 0, a.count - 1, k)
}
複製代碼

爲了保持可讀性,功能分爲三個內部函數:

  • randomPivot()選擇一個隨機數並將其放在當前分區的末尾(這是Lomuto分區方案的要求,有關詳細信息,請參閱快速排序上的討論)。

  • randomizedPartition()是Lomuto的快速排序分區方案。 完成後,隨機選擇的基準位於數組中的最終排序位置。它返回基準值的數組索引。

  • randomizedSelect()作了全部困難的工做。 它首先調用分區函數,而後決定下一步作什麼。 若是基準的索引等於咱們正在尋找的k元素,咱們就完成了。 若是k小於基準索引,它必須回到左分區中,咱們將在那裏遞歸再次嘗試。 當第k數在右分區中時,一樣如此。

很酷,對吧? 一般,快速排序是一種 O(nlogn) 算法,但因爲咱們只對數組中較小的部分進行分區,所以randomizedSelect()的運行時間爲 O(n)

注意: 此函數計算數組中第k最小項,其中k從0開始。若是你想要第k最大項,請用a.count - k

做者:Daniel Speiser,Matthijs Hollemans
翻譯:Andy Ron
校對:Andy Ron

相關文章
相關標籤/搜索