【譯】Swift演算法俱樂部-統計出現次數
本文是對ofollow,noindex">Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Club 是raywenderlich.com 網站出品的用Swift實現演算法和資料結構的開源專案,目前在GitHub上有18000+:star:️,我初略統計了一下,大概有一百左右個的演算法和資料結構,基本上常見的都包含了,是iOSer學習演算法和資料結構不錯的資源。
:octopus:andyRon/swift-algorithm-club-cn 是我對Swift Algorithm Club,邊學習邊翻譯的專案。歡迎有興趣學習演算法和資料結構,有時間的小夥伴一起參與翻譯,歡迎issue,或者直接提交pull request。
本文的翻譯原文和程式碼可以檢視:octopus:swift-algorithm-club-cn/Count Occurrences
目標:計算某個值在陣列中出現的次數。
顯而易見的方法是從陣列的開頭直到結束的Linear%20Search/" target="_blank" rel="nofollow,noindex">線性搜尋 ,計算您遇到該值的次數。 這是一個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 = 0
和high = 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