本文是對 Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Club是 raywenderlich.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 = 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。
這是一個如何修改基本二分搜尋以解決其它演算法問題的示例。 當然,它需要先對陣列進行排序。