【譯】Swift演算法俱樂部-統計出現次數

Andy_Ron發表於2019-02-27

本文是對 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/Count Occurrences


目標:計算某個值在陣列中出現的次數。

顯而易見的方法是從陣列的開頭直到結束的線性搜尋,計算您遇到該值的次數。 這是一個 O(n) 演算法。

但是,如果陣列已經排過序的,則可以通過使用修改二分搜尋來更快的完成這個任務,時間複雜度為O(logn)

假設我們有以下陣列:

[ 0, 1, 1, 3, 3, 3, 3, 6, 8, 10, 11, 11 ]
複製程式碼

如果我們想知道值3出現的次數,我們可以進行常規二分搜尋。 這可以獲得四個3索引中的一個:

[ 0, 1, 1, 3, 3, 3, 3, 6, 8, 10, 11, 11 ]
           *  *  *  *
複製程式碼

但是,這仍然沒有告訴你有多少其它的3。 要找到那些其它的3,你仍然需要在左邊進行線性搜尋,在右邊進行線性搜尋。 在大多數情況下,這將是足夠快的,但在最壞的情況下 —— 當這個陣列中除了之前的一個3之外就沒有其它3了 —— 這樣時間複雜度依然是O(n)

一個訣竅是使用兩個二分搜尋,一個用於查詢3開始(左邊界)的位置,另一個用於查詢3結束的位置(右邊界)。

程式碼如下:

func countOccurrencesOfKey(_ key: Int, inArray a: [Int]) -> Int {
  func leftBoundary() -> Int {
    var low = 0
    var high = a.count
    while low < high {
      let midIndex = low + (high - low)/2
      if a[midIndex] < key {
        low = midIndex + 1
      } else {
        high = midIndex
      }
    }
    return low
  }

  func rightBoundary() -> Int {
    var low = 0
    var high = a.count
    while low < high {
      let midIndex = low + (high - low)/2
      if a[midIndex] > key {
        high = midIndex
      } else {
        low = midIndex + 1
      }
    }
    return low
  }

  return rightBoundary() - leftBoundary()
}
複製程式碼

請注意,輔助函式leftBoundary()rightBoundary()二分搜尋演算法非常相似。最大的區別在於,當它們找到搜尋鍵時,它們不會停止,而是繼續前進。

要測試此演算法,將程式碼複製到 playground,然後執行以下操作:

let a = [ 0, 1, 1, 3, 3, 3, 3, 6, 8, 10, 11, 11 ]

countOccurrencesOfKey(3, inArray: a)  // returns 4
複製程式碼

請記住: 使用的陣列,確保已經排序過!

來看看這個例子的過程。 該陣列是:

[ 0, 1, 1, 3, 3, 3, 3, 6, 8, 10, 11, 11 ]
複製程式碼

為了找到左邊界,我們從low = 0high = 12開始。 第一個中間索引是6

[ 0, 1, 1, 3, 3, 3, 3, 6, 8, 10, 11, 11 ]
                    *
複製程式碼

通過常規二分搜尋,你現在就可以完成了,但是我們不只是檢視是否出現了值3 —— 而是想要找到它第一次出現的位置。

由於該演算法遵循與二分搜尋相同的原理,我們現在忽略陣列的右半部分並計算新的中間索引:

[ 0, 1, 1, 3, 3, 3 | x, x, x, x, x, x ]
           *
複製程式碼

我們再次找到了一個3,這是第一個。 但演算法不知道,所以我們再次拆分陣列:

[ 0, 1, 1 | x, x, x | x, x, x, x, x, x ]
     *
複製程式碼

還沒完, 再次拆分,但這次使用右半部分:

[ x, x | 1 | x, x, x | x, x, x, x, x, x ]
         *
複製程式碼

陣列不能再被拆分,這意味著左邊界在索引3處。

現在讓我們重新開始,嘗試找到右邊界。 這非常相似,所以我將向您展示不同的步驟:

[ 0, 1, 1, 3, 3, 3, 3, 6, 8, 10, 11, 11 ]
                    *

[ x, x, x, x, x, x, x | 6, 8, 10, 11, 11 ]
                              *

[ x, x, x, x, x, x, x | 6, 8, | x, x, x ]
                           *

[ x, x, x, x, x, x, x | 6 | x | x, x, x ]
                        *
複製程式碼

右邊界位於索引7處。兩個邊界之間的差異是7 – 3 = 4,因此數字3在此陣列中出現四次。

每個二分搜尋需要4個步驟,所以總共這個演算法需要8個步驟。 在僅有12個項的陣列上獲得的收益不是很大,但是陣列越大,該演算法的效率就越高。 對於具有1,000,000個專案的排序陣列,只需要2 x 20 = 40個步驟來計算任何特定值的出現次數。

順便說一句,如果你要查詢的值不在陣列中,那麼rightBoundary()leftBoundary()返回相同的值,因此它們之間的差值為0。

這是一個如何修改基本二分搜尋以解決其它演算法問題的示例。 當然,它需要先對陣列進行排序。

作者:Matthijs Hollemans
翻譯:Andy Ron
校對:Andy Ron

相關文章