Python“in”关键字对排序列表的效率
Posted
技术标签:
【中文标题】Python“in”关键字对排序列表的效率【英文标题】:Efficiency of Python "in" keyword for sorted list 【发布时间】:2016-04-14 07:23:54 【问题描述】:如果我有一个已经排序的列表并使用 in 关键字,例如:
a = [1,2,5,6,8,9,10]
print 8 in a
我认为这应该进行顺序搜索,但我可以通过二分搜索使其更快吗? 有没有一种在排序列表中搜索的 Pythonic 方式?
【问题讨论】:
'我认为这应该进行顺序搜索'。为什么你认为这是正在发生的事情? 将其转换为一个集合,然后使用“in” @Lutz 因为解释器无法神奇地判断列表是否已排序? @Lutzdef is_in(some_arr, val): return val in some_arr
- 你认为解释器应该如何判断some_arr
是否已排序。显然这是不可能的,所以它不能那样做。好吧,它可以在那里进行额外的检查,以确定列表是否已排序,然后使用二进制搜索 - 但由于这需要遍历整个列表,这反而违背了目的。
@Benjamin:只有在您想进行多个 in
测试时,转换为集合才有帮助。如果列表已排序,二等分 (O(logN)) 将优于转换为集合 (O(N))。
【参考方案1】:
标准库有bisect
模块,支持按排序顺序搜索。
但是,对于小型列表,我敢打赌 in
运算符背后的 C 实现会击败 bisect
。你必须用一堆常见的情况来衡量你的目标硬件上真正的收支平衡点......
值得注意的是,如果您可以摆脱无序迭代(即set
),那么您可以在平均O(1)
时间内进行查找(使用in
运算符),而不是二等分O(logN)
的序列和O(N)
序列上的in
运算符。而且,使用一套还可以避免首先对其进行排序的成本:-)。
【讨论】:
我做了一些测试,盈亏平衡点其实很小;大约 30 个 0-60 范围内的整数,如果一半的查找会丢失。 @AnttiHaapala -- 这听起来很合理。谢谢你这样做:-)。在 C 或 Fortran 等编译语言中进行此类测试会变得非常有趣。然后cache locality and branch prediction 可以开始真正影响您的运行时。【参考方案2】:在标准库中的模块bisect
中有对Python 的二进制搜索。不支持in
/contains
原样,但是可以写一个小函数来处理:
from bisect import bisect_left
def contains(a, x):
"""returns true if sorted sequence `a` contains `x`"""
i = bisect_left(a, x)
return i != len(a) and a[i] == x
然后
>>> contains([1,2,3], 3)
True
>>> contains([1,2,3], 4)
False
不过,这不会很快,因为 自 Python 2.4 以来,bisect
是用 Python 编写的,而不是用 C 编写的,所以在很多情况下你可能会发现顺序 in
更快。bisect
在 CPython 中具有可选的 C 加速功能。
在 CPython 中很难确定准确的盈亏平衡点。这是因为代码是用 C 编写的;如果您检查的值大于或小于序列中的任何值,那么 CPU 的分支预测将欺骗您,您会得到:
In [2]: a = list(range(100))
In [3]: %timeit contains(a, 101)
The slowest run took 8.09 times longer than the fastest. This could mean that an intermediate result is being cached
1000000 loops, best of 3: 370 ns per loop
这里,最好的 3 并不代表算法的真实运行时间。
但是通过调整测试,我得出结论,对于少至 30 个元素的列表,二等分可能比 in
更快。
但是,如果您要执行很多in
操作,则应该使用set
;您可以将列表一次转换为一个集合(甚至不排序),in
操作将比任何二分搜索都快:
>>> a = [10, 6, 8, 1, 2, 5, 9]
>>> a_set = set(a)
>>> 10 in a_set
True
另一方面,排序列表比构建集合具有更大的时间复杂性,因此大多数情况下,集合是要走的路。
【讨论】:
【参考方案3】:我会选择这个纯粹的单线(假设 bisect
被导入):
a and a[bisect.bisect_right(a, x) - 1] == x
压力测试:
from bisect import bisect_right
from random import randrange
def contains(a, x):
return a and a[bisect.bisect_right(a, x) - 1] == x
for _ in range(10000):
a = sorted(randrange(10) for _ in range(10))
x = randrange(-5, 15)
assert (x in a) == contains(a, x), f"Error for x in a"
... 不打印任何内容。
【讨论】:
以上是关于Python“in”关键字对排序列表的效率的主要内容,如果未能解决你的问题,请参考以下文章