二分查找
By lovro
TS ekse
二分查找时计算机科学中一个基础的算法。为了研究它,我们要先建立一个理论上的支柱,然后正确的用它来实现这个算法同时避免每个人都讨论过的离奇的差一错误。
在一个已排序序列中查找一个值
二分查找最简单的形式,它用于快速的在已排序序列中查找一个值(现在把这个序列认为是一个普通的数组)。我们把要查找的值叫做目标值(target)作为区别。二分查找始终维护一个原始序列的包含目标值的子序列。这个子序列叫做搜索区域(search space)。最开始搜索区域是整个序列。每一步,这个算法都会用查找区域的中间值和目标值做比较。由于这个比较和序列是已排序的,这样就排除了一半的查找区域。重复的这样做,它最后会剩下一个查找区域只包含一个元素,就是目标值。
例如,考虑下边这个已排序的递增整数序列,假如说我们要查找55:
0 | 5 | 13 | 19 | 22 | 41 | 55 | 68 | 72 | 81 | 98 |
我们想要知道的是目标值在序列中的位置所以我们将用序号来表示查找区域。最开始,查找区域包括1到11。因为查找区域是一个区间,存储两个数,小序号和大的序号。如上所描述的,我们就可以选择中间值,中间值就是序号为6的那个数(1和11的中间点):值为41,但是它要比我们要找的目标值要小。现在我们可以断定不仅序号为6所代表的数的值要比目标值小,而且在1到5之间的数都不可能是目标值,因为这个区域所有的值都要比41要小。这样就把查找区域缩小到了7到11:
55 | 68 | 72 | 81 | 98 |
用类似的方式处理,我们就去掉了第二个区域留下了:
55 | 68 |
复杂度
因为每次比较二分查找都是使用一般的查找区域,我们可以断言并轻松的证明二分查找不会使用超过(O表达式)O(log N)次比较久会找到目标值。
这个算法是一个很慢的增长函数。如果你不知道二分查找的效率有多高,考虑下边从一个有上百万姓名的电话本中查找一个名字。二分查找可 以在21次比较内系统的找到你想要的名字。如果你可以获得全球所有人已排序的名字序列,那么你可以在35次比较内找到你想要的人。这好像现在不可行和没什么用,但是我们将马上证明它。
注意这个我们可以随意访问到这个序列中的值。试着在例如像链表的容器上就会没有什么效果,却用线性查找代替还更好。
标准库中的二分查找
C++的标准类库实现了二分查找在lower_bound、upper_bound、binary_search和equal_range,根据你实际想要做的。Java也有一个内置数组。数组的二分查找和.NET框架下有Array.BinarySearch。
你最好在可能的情况下使用库函数,因为,就像你知道的,自己实现一个二分查找可能会有错误。
除了数组:离散的二分查找
我们从这里开始抽象的讨论二分查找。一个序列(数组)只是映射了一个整数(索引)和对应的值的函数。然后,没有理由来限制我们把二分查找运用到实际的序列中。实际上,我们可以把上边讨论的算法使用在任何有一个整数集合定义域的线性函数f。唯一的不同是我们用一个判等函数来查找这个数组:我们要做的是找到x满足f(x)等于目标值。查找区域就是一个更普通的定义域的一个子集,同时我们要找的目标值就在对应的定义域中。二分查找的力量现在开始显现出来:我们不仅最多只需O(log N)次比较来找到目标值,而且我们不用多次的求这个函数的值。更进一步,我们不受实际数量例如可用内存,同数组一样。