使用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,这样可以防止rightleft都很大时,(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) 的插入排序,这里就抛砖引玉,暂时不细写了。

参考资料:

  1. bisect 官方文档
posted @ 2020-02-06 19:25  AdjWang  阅读(488)  评论(0编辑  收藏  举报