二分查找(binary_search)
仅当列表为有序且为顺序存储时,二分查找才适用
如果一个序列是无序的或者是链表,那么该序列就不能进行二分查找。之所以被查找的序列要满足这样的条件,是由二分查找算法的原理决定的。
本文就来探究几个最常用的二分查找场景:寻找一个数在数组中的下标、寻找左侧边界、寻找右侧边界。
二分查找中的几个细节:
while循环中的不等号是否应该带等号;
mid 是否应该加一
python版二分查找
# 二分查找:在给定数组中,寻找特定元素的下标
def binary_search(list, elem):
'''
:param list: ordered one dimensional array
:param elem: need to search
:return: elem's index in list
'''
low = 0;
high = len(list) - 1;
while low<=high:
mid = (high+low)//2; #如果(low+high)不是整数,自动将mid向下取整
if elem == list[mid]:
return mid
elif elem>list[mid]:
low = mid+1;
else:
high = mid - 1
return None
list = [1,2,3,4,5]
list1 = [1,2,3,4,5,6]
elem1Index = binary_search(list,3)
elem2Index = binary_search(list1,4)
print(elem1Index, elem2Index)# 2 3
细节详解
- while 循环的条件中是 <=,而不是 < ?
初始化时, low 的赋值为第一个元素的索引 0; high 的赋值是最后一个1元素的索引 list.length - 1。
算法的搜索区间(search space)为: [low, high]
while(low <= high)的终止条件是 low == high + 1,写成区间的形式就是 [high+ 1, high],或者带个具体的数字进去 [3, 2],可见这时候搜索区间为空,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 None 即可。
while(low < high)的终止条件是 low == high,写成区间的形式就是 [high, high],或者带个具体的数字进去 [2, 2],这时候搜索区间非空,还有一个数 2,但此时 while 循环终止了。也就是说这区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就可能出现错误。
算法的性能
折半查找的运行过程可以用二叉树来描述,这棵树通常称为“判定树”。例如 [ 5 13 19 21 37 56 64 75 80 88 92 ] 对应的判定树如图
在判定树中可以看到,如果想在查找表中查找 21 的位置,只需要进行 3 次比较,依次和 56、19、21 进行比较,而比较的次数恰好是该关键字所在判定树中的层次(关键字 21 在判定树中的第 3 层)。
对于具有 n 个结点(查找表中含有 n 个关键字)的判定树,它的层次数至多为: log 2 x + 1 {{\log }_{2}}x+1 log2x+1(如果结果不是整数,则做取整操作,例如: log 2 11 + 1 = 3 + 1 = 4 {{\log_2}11} +1 = 3 + 1 = 4 log211+1=3+1=4 )。
同时,在查找表中各个关键字被查找概率相同的情况下,折半查找的平均查找长度为: A S L = l o g 2 ( n + 1 ) – 1 ASL = log_2(n+1) – 1 ASL=log2(n+1)–1。
空间复杂度
在我们的实现中,二分查找对于存储空间的要求是只需要能存储low、high、mid、target、数组地址(参数array)和数组元素数量(参数n)这6个局部变量就行了,因此它对存储空间的要求是常数数量,不随着元素多少而变化。所以它的空间复杂度为O(1)。
算法局限性
(1) 需要寻找的特定元素在有序数组存在多个时,无法确定返回第一个特定元素的下标。
例:
list2 = [1,2,2,2,2,5]
elem3Index = binary_search(list2,2)
print(elem3Index) # 2 # 注意此时返回的不是首个 2 的索引
此时算法返回的索引是 2。但是一般情况下我们需要的第一个特定元素1的小标,正确结果应该是 1 。
这样的需求很常见。你也许会说,找到一个 target 索引,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的时间复杂度了。
(2) 该算法的使用的前提是静态查找表中的数据必须是有序的。
同时仅限于查找表用顺序存储结构表示。当查找表使用链式存储结构表示时,折半查找算法无法有效地进行比较操作(排序和查找操作的实现都异常繁琐)。
寻找左侧边界的二分搜索
即寻找第一个特定元素的索引
待续。。。。。。
寻找右侧边界的二分搜索
即寻找最后一个特定元素的索引