已排序的序列可以用来进行快速搜索,而标准库的 bisect
模块给我们提供了二分查找算法。bisect.insort
让已排序的序列保持有序
bisect
模块包含两个主要函数,bisect
和 insort
,两个函数都利用二分查找算法来在有序序列中查找或插入元素。
用bisect来搜索
bisect(haystack, needle)
在 haystack
(干草垛)里搜索 needle
(针)的位置,该位置满足的条件是,把 needle
插入这个位置之后,haystack
还能保持升序。也就是在说这个函数返回的位置前面的值,都小于或等于 needle
的值。其中 haystack
必须是一个有序的序列。你可以先用 bisect(haystack, needle)
查找位置 index
,再用 haystack.insert(index, needle)
来插入新值。但你也可用 insort
来一步到位,并且后者的速度更快一些。
Python 的高产贡献者 Raymond Hettinger 写了一个排序集合模块(http://code.activestate.com/recipes/577197-sortedcollection/
),模块里集成了 bisect
功能,但是比独立的 bisect
更易用。
示例: 利用几个精心挑选的 needle,向我们展示了 bisect 返回的不同位置值。这段代码的输出结果显示在下图中。
# -*- coding: utf-8 -*-
import bisect
import sys
HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]
NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]
ROW_FMT = '{0:2d} @ {1:2d} {2}{0:<2d}'
def demo(bisect_fn):
for needle in reversed(NEEDLES):
position = bisect_fn(HAYSTACK, needle) # 用特定的 bisect 函数来计算元素应该出现的位置。
offset = position * ' |' # 利用该位置来算出需要几个分隔符号
print(ROW_FMT.format(needle, position, offset)) # 把元素和其应该出现的位置打印出来。
if __name__ == '__main__':
if sys.argv[-1] == 'left': # 根据命令上最后一个参数来选用 bisect 函数。
bisect_fn = bisect.bisect_left
else:
bisect_fn = bisect.bisect
print('DEMO:', bisect_fn.__name__) # 根据命令上最后一个参数来选用 bisect 函数。
print('haystack ->', ' '.join('%2d' % n for n in HAYSTACK))
demo(bisect_fn)
每一行以 needle @ position(元素及其应该插入的位置)开始,然后展示了该元素在原序列中的物理位置
bisect 的表现可以从两个方面来调教。
首先可以用它的两个可选参数——lo 和 hi——来缩小搜寻的范围。lo 的默认值是 0,hi的默认值是序列的长度,即 len() 作用于该序列的返回值。
其次,bisect 函数其实是 bisect_right 函数的别名,后者还有个姊妹函数叫 bisect_left。它们的区别在于,bisect_left 返回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素会被放置于它相等的元素的前面,而 bisect_right 返回的则是跟它相等的元素之后的位置。这个细微的差别可能对于整数序列来讲没什么用,但是对于那些值相等但是形式不同的数据类型来讲,结果就不一样了。比如说虽然 1 == 1.0 的返回值是 True,1 和 1.0其实是两个不同的元素。下图显示的是用 bisect_left 来运行上述示例的结果。
跟上图对比可以发现,值 1、8、23、29 和 30 的插入位置变成了原序列中这些值的前面
bisect 可以用来建立一个用数字作为索引的查询表格,比如说把分数和成绩对应起来
# 根据一个分数,找到它所对应的成绩
def grade(score):
breakpoints = [60, 70, 80, 90]
grades = 'FDCBA'
i = bisect.bisect(breakpoints, score)
return grades[i]
if __name__ == '__main__':
print([grade(score) for score in [33, 99, 77, 70, 89, 90, 100]])
# ['F', 'A', 'C', 'C', 'B', 'A', 'A']
上面示例里的代码来自 bisect
模块的文档(https://docs.python.org/3/library/bisect.html
)。文档里列举了一些利用 bisect
的函数,它们可以在很长的有序序列中作为 index
的替代,用来更快地查找一个元素的位置
这些函数不但可以用于查找,还可以用来向序列中插入新元素,下面就来看看它们的用法。
用bisect.insort插入新元素
insort(seq, item)
把变量 item
插入到序列 seq
中,并能保持 seq
的升序顺序。详见示例如下。
# -*- coding: utf-8 -*-
import bisect
import random
SIZE = 7
random.seed(1729)
my_list = []
for i in range(SIZE):
new_item = random.randrange(SIZE * 2)
bisect.insort(my_list, new_item)
print('%2d ->' % new_item, my_list)
insort 跟 bisect 一样,有 lo 和 hi 两个可选参数用来控制查找的范围。它也有个变体叫insort_left,这个变体在背后用的是 bisect_left。
目前所提到的内容都不仅仅是对列表或者元组有效,还可以应用于几乎所有的序列类型上。有时候因为列表实在是太方便了,所以 Python 程序员可能会过度使用它,反正我知道我犯过这个毛病。而如果你只需要处理数字列表的话,数组可能是个更好的选择。以后就来讨论一些可以替换列表的数据结构。