本文是对 Swift Algorithm Club 翻译的一篇文章。git
Swift Algorithm Club是 raywenderlich.com网站出品的用Swift实现算法和数据结构的开源项目,目前在GitHub上有18000+⭐️,我初略统计了一下,大概有一百左右个的算法和数据结构,基本上常见的都包含了,是iOSer学习算法和数据结构不错的资源。github
🐙andyRon/swift-algorithm-club-cn是我对Swift Algorithm Club,边学习边翻译的项目。因为能力有限,如发现错误或翻译不妥,请指正,欢迎pull request。也欢迎有兴趣、有时间的小伙伴一块儿参与翻译和学习🤓。固然也欢迎加⭐️,🤩🤩🤩🤨🤪。算法
本文的翻译原文和代码能够查看🐙swift-algorithm-club-cn/Count Occurrencesswift
目标:计算某个值在数组中出现的次数。数组
显而易见的方法是从数组的开头直到结束的线性搜索,计算您遇到该值的次数。 这是一个 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。
这是一个如何修改基本二分搜索以解决其它算法问题的示例。 固然,它须要先对数组进行排序。