bisect —- 数组二分查找算法

源代码: Lib/bisect.py


这个模块对有序列表提供了支持,使得他们可以在插入新数据仍然保持有序。对于长列表,如果其包含元素的比较操作十分昂贵的话,这可以是对更常见方法的改进。这个模块叫做 bisect 因为其使用了基本的二分(bisection)算法。源代码也可以作为很棒的算法示例(边界判断也做好啦!)

定义了以下函数:

bisect.bisect_left(a, x, lo=0, hi=len(a), **, key=None*)

a 中找到 x 合适的插入点以维持有序。参数 lohi 可以被用于确定需要考虑的子集;默认情况下整个列表都会被使用。如果 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]) 在右半边。

key 指定带有单个参数的 key function,用于从每个输入元素中提取比较键。 默认值为 None (直接比较元素)。

在 3.10 版更改: 增加了 key 形参。

bisect.bisect_right(a, x, lo=0, hi=len(a), **, key=None*)

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])

key 指定带有单个参数的 key function,用于从每个输入元素中提取比较键。 默认值为 None (直接比较元素)。

在 3.10 版更改: 增加了 key 形参。

bisect.insort_left(a, x, lo=0, hi=len(a), **, key=None*)

按照已排序顺序将 x 插入到 a 中。

key 指定带有单个参数的 key function,用于从每个输入元素中提取比较键。 默认值为 None (直接比较元素)。

此函数首先会运行 bisect_left() 来定位一个插入点。 然后,它会在 a 上运行 insert() 方法在正确的位置插入 x 以保持排序顺序。

请记住 O(log n) 搜索是由缓慢的 O(n) 抛入步骤主导的。

在 3.10 版更改: 增加了 key 形参。

bisect.insort_right(a, x, lo=0, hi=len(a), **, key=None*)

bisect.insort(a, x, lo=0, hi=len(a))

类似于 insort_left(),但是把 x 插入到 a 中已存在元素 x 的右侧。

key 指定带有单个参数的 key function,用于从每个输入元素中提取比较键。 默认值为 None (直接比较元素)。

此函数首先会运行 bisect_right() 来定位一个插入点。 然后,它会在 a 上运行 insert() 方法在正确的位置插入 x 以保持排序顺序。

请记住 O(log n) 搜索是由缓慢的 O(n) 抛入步骤主导的。

在 3.10 版更改: 增加了 key 形参。

性能说明

当使用 bisect()insort() 编写时间敏感的代码时,请记住以下概念。

  • 二分法对于搜索一定范围的值是很高效的。 对于定位特定的值,则字典的性能更好。

  • insort() 函数的时间复杂度为 O(n) 因为对数时间的搜索步骤被线性时间的插入步骤所主导。

  • 这些搜索函数都是无状态的并且会在它们被使用后丢弃键函数的结果。 因此,如果在一个循环中使用搜索函数,则键函数可能会在同一个数据元素上被反复调用。 如果键函数速度不快,请考虑用 functools.cache() 来包装它以避免重复计算。 另外,也可以考虑搜索一个预先计算好的键数组来定位插入点(如下面的示例节所演示的)。

参见

  • Sorted Collections 是一个使用 bisect 来管理数据的已排序多项集的高性能模块。

  • SortedCollection recipe 使用 bisect 构建了一个功能完整的多项集类,拥有直观的搜索方法和对键函数的支持。 所有键函数都 是预先计算好的以避免在搜索期间对键函数的不必要的调用。

搜索有序列表

上面的 bisect() 函数对于找到插入点是有用的,但在一般的搜索任务中可能会有点尴尬。下面 5 个函数展示了如何将其转变成有序列表中的标准查找函数

  1. def index(a, x):
  2. 'Locate the leftmost value exactly equal to x'
  3. i = bisect_left(a, x)
  4. if i != len(a) and a[i] == x:
  5. return i
  6. raise ValueError
  7. def find_lt(a, x):
  8. 'Find rightmost value less than x'
  9. i = bisect_left(a, x)
  10. if i:
  11. return a[i-1]
  12. raise ValueError
  13. def find_le(a, x):
  14. 'Find rightmost value less than or equal to x'
  15. i = bisect_right(a, x)
  16. if i:
  17. return a[i-1]
  18. raise ValueError
  19. def find_gt(a, x):
  20. 'Find leftmost value greater than x'
  21. i = bisect_right(a, x)
  22. if i != len(a):
  23. return a[i]
  24. raise ValueError
  25. def find_ge(a, x):
  26. 'Find leftmost item greater than or equal to x'
  27. i = bisect_left(a, x)
  28. if i != len(a):
  29. return a[i]
  30. raise ValueError

例子

函数 bisect() 还可以用于数字表查询。这个例子是使用 bisect() 从一个给定的考试成绩集合里,通过一个有序数字表,查出其对应的字母等级:90 分及以上是 ‘A’,80 到 89 是 ‘B’,以此类推

  1. >>> def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
  2. ... i = bisect(breakpoints, score)
  3. ... return grades[i]
  4. ...
  5. >>> [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]
  6. ['F', 'A', 'C', 'C', 'B', 'A', 'A']

一种避免重复调用键函数的技巧是搜索一个预先计算好的键函数列表来找出记录的索引:

  1. >>> data = [('red', 5), ('blue', 1), ('yellow', 8), ('black', 0)]
  2. >>> data.sort(key=lambda r: r[1]) # Or use operator.itemgetter(1).
  3. >>> keys = [r[1] for r in data] # Precompute a list of keys.
  4. >>> data[bisect_left(keys, 0)]
  5. ('black', 0)
  6. >>> data[bisect_left(keys, 1)]
  7. ('blue', 1)
  8. >>> data[bisect_left(keys, 5)]
  9. ('red', 5)
  10. >>> data[bisect_left(keys, 8)]
  11. ('yellow', 8)