[你能用 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学习资料》免费赠送🆓!(安全链接,放心点击)
若有侵权,请联系删除

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/12405.html