使用bisect库实现二分查找
手动实现
假如有一个有序表nums
,怎么样在nums
里找到某个值的位置呢?没错,就是nums.index(k)
,哈哈哈哈哈哈哈……
假如nums
很长很长,那就要祭出二分查找了
def binary_search(nums: List[int], k: int) -> int:
if not nums:
raise ValueError('list is empty')
left, right = 0, len(nums)-1
while left <= right:
mid = (right + left) // 2
if k < nums[mid]:
right = mid - 1
elif k > nums[mid]:
left = mid + 1
else: # if k == nums[mid]:
return mid
raise ValueError(f'{k} is not in list')
计算mid的公式为
(right + left) / 2
,动态语言随便写,但是在静态语言中建议写成left + (right - left) / 2
,这样可以防止right
和left
都很大时,(right + left)
溢出。
使用bisect
二分查找可以给查找加速,但是每次写这么一段代码也够心烦。其实Python有这么一个库:bisect 这个库实现了二分查找和二分插入。
bisect的方法数量不多,参考官方文档给出的说明:
-
bisect.bisect_left(a, x, lo=0, hi=len(a))
在 a 中找到 x 合适的插入点以维持有序。参数 lo 和 hi 可以被用于确定需要考虑的子集;默认情况下整个列表都会被使用。如果 x 已经在 a 里存在,那么插入点会在已存在元素之前(也就是左边)。如果 a 是列表(list)的话,返回值是可以被放在 list.insert() 的第一个参数的。
返回的插入点 i 可以将数组 a 分成两部分。左侧是 all(val < x for val in a[lo:i]) ,右侧是 all(val >= x for val in a[i:hi]) 。 -
bisect.bisect_right(a, x, lo=0, hi=len(a))
bisect.bisect(a, x, lo=0, hi=len(a))
类似于 bisect_left(),但是返回的插入点是 a 中已存在元素 x 的右侧。
返回的插入点 i 可以将数组 a 分成两部分。左侧是 all(val <= x for val in a[lo:i]),右侧是 all(val > x for val in a[i:hi]) for the right side。 -
bisect.insort_left(a, x, lo=0, hi=len(a))
将 x 插入到一个有序序列 a 里,并维持其有序。如果 a 有序的话,这相当于 a.insert(bisect.bisect_left(a, x, lo, hi), x)。要注意搜索是 O(log n) 的,插入却是 O(n) 的。 -
bisect.insort_right(a, x, lo=0, hi=len(a))
bisect.insort(a, x, lo=0, hi=len(a))
类似于 insort_left(),但是把 x 插入到 a 中已存在元素 x 的右侧。
参见: SortedCollection recipe 使用 bisect 构造了一个功能完整的集合类,提供了直接的搜索方法和对用于搜索的 key 方法的支持。所有用于搜索的键都是预先计算的,以避免在搜索时对 key 方法的不必要调用。
搜索有序列表
上面的 bisect() 函数对于找到插入点是有用的,但在一般的搜索任务中可能会有点尴尬。下面 5 个函数展示了如何将其转变成有序列表中的标准查找函数
def index(a, x):
'Locate the leftmost value exactly equal to x'
i = bisect_left(a, x)
if i != len(a) and a[i] == x:
return i
raise ValueError
def find_lt(a, x):
'Find rightmost value less than x'
i = bisect_left(a, x)
if i:
return a[i-1]
raise ValueError
def find_le(a, x):
'Find rightmost value less than or equal to x'
i = bisect_right(a, x)
if i:
return a[i-1]
raise ValueError
def find_gt(a, x):
'Find leftmost value greater than x'
i = bisect_right(a, x)
if i != len(a):
return a[i]
raise ValueError
def find_ge(a, x):
'Find leftmost item greater than or equal to x'
i = bisect_left(a, x)
if i != len(a):
return a[i]
raise ValueError
以上内容摘自官方文档,官方文档有了中文版以后可太友好了。然后照猫画虎实现一个新的binary_search函数,参考上面的index()
def binary_search(nums: List[int], k: int) -> int:
if not nums:
raise ValueError('list is empty')
i = bisect.bisect_left(nums, k)
if i != len(nums) and nums[i] == k:
return i
raise ValueError(f'{k} is not in list')
是不是比手动实现的二分查找简洁多了?😉
测试一下:
>>> from typing import List
>>> import bisect
>>> def binary_search_bisect(nums: List[int], k: int) -> int:
... if not nums:
... raise ValueError('list is empty')
... i = bisect.bisect_left(nums, k)
... if i != len(nums) and nums[i] == k:
... return i
... raise ValueError(f'{k} is not in list')
...
>>> binary_search_bisect([0, 1, 2, 3, 4, 5], 3)
3
>>> binary_search_bisect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 7)
7
>>> binary_search_bisect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)
9
>>> binary_search_bisect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in binary_search_bisect
ValueError: 10 is not in list
>>> binary_search_bisect([], 10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in binary_search_bisect
ValueError: list is empty
完美~
(功能实现了,性能怎么样呢?下下下篇看看它的速度和手动实现的差别有多大,顺便说下装饰器。)
此外,这个库因为有bisect.insort(a, x, lo=0, hi=len(a))
这样的方法,所以也可以直接拿去实现时间复杂度为 O(nlog n) 的插入排序,这里就抛砖引玉,暂时不细写了。
参考资料: