你能用 Python 的 bisect 模块做到这些事

本文介绍了Python的bisect模块,包括其两个主要函数bisect_left和insort_left,展示了如何用它进行高效搜索、保持数据排序,以及在复杂情况下的应用,如二分查找、处理连续相等值、区间映射、字典搜索和自定义对象排序。
摘要由CSDN通过智能技术生成

[你能用 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 合适的插入点以维持有序。参数 lohi 可以被用于确定需要考虑的子集;默认情况下整个列表都会被使用。如果 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 值。

如果 keyNone,则将直接比较元素而不调用任何键函数。

在 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 模块中的函数遵循相同的模式。也就是说,我们在索引 lohi 之间的列表 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_leftin 运算符快得多。

它是二分搜索自然意味着它在 O(log(n)) 中运行,但它通过在 C 中预编译而得到进一步帮助,因此它总是比您自己编写的任何内容都要快。

---------------------------END---------------------------

题外话

在这里插入图片描述

感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。

👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)

一、Python所有方向的学习路线

Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。

img
img

二、Python必备开发工具

工具都帮大家整理好了,安装就可直接上手!img

三、最新Python学习笔记

当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。

img

四、Python视频合集

观看全面零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。

img

五、实战案例

纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

img

六、面试宝典

在这里插入图片描述

👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)

若有侵权,请联系删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值