[你能用 Python 的 bisect 模块做到这些事]
翻译自:(martinheinz.dev/blog/106)
虽然 Python 的 bisect
模块非常简单 - 仅包含 2 个函数 - 但我们可以用它做很多事情,包括高效搜索数据、保持数据排序等等 - 在本文中我们将探讨所有的!
bisect_left
bisect.bisect_left(a, x, lo=0, hi=len(a), ***, key=None)
在 a 中找到 x 合适的插入点以维持有序。参数 lo 和 hi 可以被用于确定需要考虑的子集;默认情况下整个列表都会被使用。如果 x 已经在 a 里存在,那么插入点会在已存在元素之前(也就是左边)。如果 a 是列表(list)的话,返回值是可以被放在
list.insert()
的第一个参数的。返回的插入点 ip 将数组 a 分为两个切片使得对于左侧切片
all(elem < x for elem in a[lo : ip])
为真值而对于右侧切片all(elem >= x for elem in a[ip : hi])
为真值。key 指定带有单个参数的 key function 用来从数组的每个元素中提取比较键。 为了支持搜索复杂记录,键函数不会被应用到 x 值。
如果 key 为
None
,则将直接比较元素而不调用任何键函数。在 3.10 版更改: 增加了 key 形参。
insort_left
bisect.insort_left(a, x, lo=0, hi=len(a), ***, key=None)
按照已排序顺序将 x 插入到 a 中。
此函数首先会运行
bisect_left()
来定位一个插入点。 然后,它会在 a 上运行insert()
方法在适当的位置插入 x 以保持排序顺序。为了支持将记录插入到表中,key 函数(如果存在)将被应用到 x 用于搜索步骤但不会用于插入步骤。
请记住
O(log n)
搜索是由缓慢的 O(n) 抛入步骤主导的。在 3.10 版更改: 增加了 key 形参。
什么是二等分(离子)?
在我们开始使用该模块之前,让我们首先解释一下 bisect(ion) 实际上是什么。官方定义:
Bisection is the division of a given curve, figure, or interval into two equal parts (halves). 二分是将给定的曲线、图形或区间分成两个相等的部分(两半)。
用简单的英语来说,这意味着它实现了二分搜索。在实践中,这意味着我们可以使用它 - 例如 - 将元素插入到列表中,同时维护列表的排序顺序:
import bisect
some_list = [0, 6, 1, 5, 8, 2]
some_list.sort()
print(some_list) # [0, 1, 2, 5, 6, 8]
i = bisect.bisect_left(some_list, 4)
print(i) # 3
some_list.insert(i, 4)
print(some_list) # [0, 1, 2, 4, 5, 6, 8]
# 或者,我们可以直接使用 `insort_left` 函数
bisect.insort_left(some_list, 4)
print(some_list) # [0, 1, 2, 4, 5, 6, 8]
在这个基本示例中,我们首先对列表进行排序,因为我们只能在排序的可迭代对象上使用 bisect
中的函数。然后,我们在列表上使用 bisect_left
来查找应插入第二个参数 ( 4
) 以维持排序顺序的索引。然后我们继续这样做 - 在索引 3
处插入数字 4
。或者,我们可以直接使用 insort_left
函数,该函数首先在内部使用 bisect_left
,然后也执行 insert
。
嗯,这很酷,但是您为什么要关心这个模块呢?好吧,让我向您展示您可以用它做的所有有用的事情…
二分查找
正如已经提到的, bisect
实现了二分搜索,因此它最明显的用途就是:
from bisect import bisect_left
def binary_search(a, x, lo=0, hi=len(a)):
pos = bisect_left(a, x, lo, hi) # find insertion position
return pos if pos != hi and a[pos] == x else -1 # don't walk off the end
print(binary_search([0, 1, 2, 5, 6, 8], 5)) # 3
print(binary_search([0, 1, 2, 5, 6, 8], 4)) # -1
上述 binary_search
函数的参数与 bisect
模块中的函数遵循相同的模式。也就是说,我们在索引 lo
和 hi
之间的列表 a
中查找值 x
。
唯一有趣的行是 return
语句,我们在其中测试值 x
是否确实在列表中,如果是,我们返回其位置,否则返回 -1
.
连续的相等值
然而,我们可以使用 bisect
模块做更有趣的事情,例如在列表中查找连续的相等值:
from bisect import bisect_left, bisect_right
some_list = [5, 10, 15, 15, 15, 20, 25, 40]
# Find all the 15's
value = 15
start = bisect_left(some_list, value)
end = bisect_right(some_list, value)
print(f'Successive values of {value} from index {start} to {end}: {some_list[start:end]}')
# Successive values of 15 from index 2 to 5: [15, 15, 15]
我们已经见过 bisect_left
,所以这里我们还介绍 bisect_right
,它做同样的事情,但从另一端。这样我们就可以找到我们正在寻找的连续值的开始和结束位置。
从区间到值的映射
现在,假设我们有一系列间隔/范围,并且我们想要返回相应的 ID/值。一个天真的、丑陋的解决方案可能看起来像这样:
def interval_to_value(val):
if val <= 100:
return 0
elif 100 < val <= 300:
return 1
elif 300 < val <= 500:
return 2
elif 500 < val <= 800:
return 3
elif 800 < val <= 1000:
return 4
elif val > 1000:
return 5
但是,使用 bisect_left
有一个更好的解决方案:
def interval_to_value(val):
return bisect_left([100, 300, 500, 800, 1000], val)
这不仅是非常干净的解决方案,而且速度非常快。如果您需要非自然排序,或者例如您想返回不同的内容(例如字符串),也可以对其进行扩展:
i = interval_to_value(325)
a = ['absent', 'low', 'average', 'high', 'very high', 'extreme']
print(a[i]) # average
字典中最近的键
现在,假设我们有一个字典形式的映射,并且我们想要查找指定键的值。如果该键存在那就很酷,但如果不存在,那么我们想返回最接近的键的值:
import collections
some_dict = collections.OrderedDict(
[(0, 0), (2, 1), (4, 4), (6, 9), (8, 16), (10, 25), (12, 36), (14, 49), (16, 64), (18, 81)]
)
target = 10.5
index = bisect_left(list(some_dict.keys()), target) # 6
items = list(some_dict.items())
distance1 = items[index][0] - target
distance2 = target - items[index-1][0]
# Check which one is closer:
print(f'Distance for to index {index}: {distance1}') # Distance for to index 6: 1.5
print(f'Distance for to index {index-1}: {distance2}') # Distance for to index 5: 0.5
print('Closest value:')
if distance1 < distance2:
print(items[index])
else:
print(items[index-1])
# Closest value: (10, 25)
这里我们使用 OrderedDict
来确保密钥的顺序正确。然后我们对它们使用 bisect_left
来查找插入点。最后,我们需要检查下一个或上一个索引是否更接近 target
。
前缀搜索
您可以使用 bisect
进行的另一件事是前缀搜索 - 假设我们有一个非常大的单词列表,并且想要根据给定的前缀查找单词:
def prefix_search(wordlist, prefix):
try:
index = bisect_left(wordlist, prefix)
return wordlist[index].startswith(prefix)
except IndexError:
return False
words = ['another', 'data', 'date', 'hello', 'text', 'word']
print(prefix_search(words, 'dat')) # True
print(prefix_search(words, 'xy')) # False
上面的函数仅检查列表中是否存在具有指定前缀的单词,但是可以轻松修改为从 index
循环并返回以 prefix
开头的所有单词。
如果您有足够大的单词列表,那么与仅从头开始迭代列表相比,使用 bisect
会更快。
排序的自定义对象
到目前为止,我们仅使用了内置类型,但 bisect
模块中的函数也可以应用于自定义类型。假设我们有一个自定义对象列表,并且我们希望根据某些属性维护它们在列表中的顺序:
from bisect import insort_left
class CustomObject:
def __init__(self, val):
self.prop = val # The value to compare
def __lt__(self, other):
return self.prop < other.prop
def __repr__(self):
return 'CustomObject({})'.format(self.prop)
some_objects = sorted([CustomObject(7), CustomObject(1), CustomObject(3), CustomObject(9)])
insort_left(some_objects, CustomObject(2))
print(some_objects) # [CustomObject(1), CustomObject(2), CustomObject(3), CustomObject(7), CustomObject(9)]
此代码片段利用了 bisect
使用 __lt__
魔术方法来比较对象的事实。但是,必须始终使用 bisect
函数可能会有点不方便,为了避免您可以实现这个 更完整的示例/配方中描述的 SortedCollection
。
key
函数
bisect
模块中的函数还支持使用 key
函数参数进行更复杂的比较/搜索:
some_list = [1, 3, 7, 16, 25]
some_list.reverse()
insort_left(some_list, 10, key=lambda x: -1 * x)
print(some_list) # [25, 16, 10, 7, 3, 1]
这里我们使用 key
函数来实现逆序二分搜索,只要记住列表也必须以逆序开始排序。
与逆序排序类似,人们可能也倾向于使用 key 函数来搜索元组列表,但是可以这样做:
list_of_tuples = [(1, 3), (3, 8), (5, 4), (10, 12)]
index = bisect_left(list_of_tuples, (5, )) # 2
print(list_of_tuples[2]) # (5, 4)
省略 tuple
中的第二个值会强制 bisect_left
仅根据第一个值进行比较。如果您想明确地使用 key
函数,那么 key=lambda i: i[0]
也可以。
话虽如此,key
函数能有点不直观 - 人们会期望它像例如 sorted
功能:
some_tuples = [
(0, 10),
(2, 12),
(3, 15),
(5, 20),
]
print(sorted(some_tuples, key=lambda t: t[0] + t[1])) # Works
但它没有:
index = bisect_left(some_tuples, (4, 17), key=lambda t: t[0] + t[1]) # Doesn't work
# Expectation: index = 3
# Reality: "TypeError: '<' not supported between instances of 'int' and 'tuple'"
def key_func(t):
return t[0] + t[1]
index = bisect_left(some_tuples, key_func((4, 17)), key=key_func)
print(index) # 3
相反,我们必须定义一个关键函数,将其作为 key
参数传递,并在第二个参数上调用它。
那是因为(来自文档):
key 指定一个参数的键函数,用于从数组中的每个元素中提取比较键。为了支持搜索复杂记录,key 函数不应用于 x 值。***
另请参阅 cpython
存储库中的此 GitHub 问题 ,了解此设计决策背后的推理。
结论
在我看来,对于这么小的模块,你可以用它做很多事情。
另外,除了您可以使用 bisect
执行的所有上述有用操作之外,我想强调一下它的速度有多快 - 特别是如果您的列表已经排序。例如,考虑 Stack Overflow 上的这个示例 ,显示 bisect_left
比 in
运算符快得多。
它是二分搜索自然意味着它在 O(log(n))
中运行,但它通过在 C 中预编译而得到进一步帮助,因此它总是比您自己编写的任何内容都要快。
题外话
感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。
👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)
一、Python所有方向的学习路线
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。
二、Python必备开发工具
工具都帮大家整理好了,安装就可直接上手!
三、最新Python学习笔记
当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。
四、Python视频合集
观看全面零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
五、实战案例
纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
六、面试宝典
👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)
若有侵权,请联系删除