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 因为解释器无法神奇地判断列表是否已排序? @Lutz def 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

不过,这不会很快,因为bisect 是用 Python 编写的,而不是用 C 编写的,所以在很多情况下你可能会发现顺序 in 更快。 自 Python 2.4 以来,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”关键字对排序列表的效率的主要内容,如果未能解决你的问题,请参考以下文章

无法理解键在对python列表进行排序中的功能

python实现字符串列表排序?

python怎样用自定义函数对列表排序?

python 内置排序函数使用

python中的列表及numpy数组排序

如何在python中创建二维列表