最近我在python here
上看到了一个问题1:Python If statement and logical operator issue。评论中有人给出了答案,可以这样做:
1 in (1, 2, 3)
检查项目集合中是否存在1。但据我说,这应该快得多1 in {1, 2, 3}
。正如你在讨论中看到的那样,一个声誉很高的人继续说( )
对于固定大小的输入更快。查找速度比{ }
快。我在这里问它是因为我想知道我自己的理解哪一个是正确的,我也不知道( )
是fiexd-size
还是variable size
。我只是要求对这个原始问题进行反思,这样我就可以纠正自己,如果我错了,但是用户在清除我的计算机科学知识的基础知识时没有对lookup in Tuple is O(1)
的论点进行单独的反思。所以我要问它在这里。
答案 0 :(得分:6)
当您说O(n)
之类的内容时,您必须说明n
是什么。这里,n
是元组的长度......但元组不是输入。你不是把元组作为一个论点或任何东西。 n
在您关联的对话中始终为2
,对于您的示例元组,3
始终为n
,因此对于此特定O(n)
,O(2)
与{ {1}}或O(1)
。
正如您现在可能已经注意到的那样,当O(n)
为常数时,谈论n
没有多大意义。如果你有像
def in_(element, tup):
return element in tup
你可以说运行时是O(n)
元素比较,其中n
是len(tup)
,但对于像
usr in ('Y', 'y')
谈论n
并不是很有用。
答案 1 :(得分:1)
虽然另一位评论者在技术上是正确的,x in <constant expression>
在运行时是O(1),但比较相同大小的集合和元组的性能仍然很有趣。讨论可以概括为考虑包含x in {a, b, c, ...}
形式的表达式的不同的程序。如果表达式由n
项组成,那么n
可以被视为所有可能的此类程序中的大O分析的输入。 (如果仍有人坚持认为可以在运行时提供不同的n
,那么只需想象使用exec
创建该函数。)
这些表达式的性能问题是Python运行时必须构造一次性集以便在in
测试中使用,然后立即丢弃它。这在生成的程序集中清晰可见:
>>> import dis
>>> def is_valid(x):
... return x in {1, 2, 3}
...
>>> dis.dis(is_valid)
2 0 LOAD_FAST 0 (x)
3 LOAD_CONST 1 (1)
6 LOAD_CONST 2 (2)
9 LOAD_CONST 3 (3)
12 BUILD_SET 3
15 COMPARE_OP 6 (in)
18 RETURN_VALUE
构造一组n
元素显然具有至少O(n)的成本。换句话说,使用一个实现为文字常量的集合的测试是不 O(1),因为解释器必须构造集合。这就是评论者通过参考构建的成本来试图解决的问题。
事实上,它比那更奇怪;由于Python VM的性质,允许编译器在编译时构造仅包含数字文字的元组,它可以:
>>> import dis
>>> def is_valid(x):
... return x in (1, 2, 3)
...
>>> dis.dis(is_valid)
2 0 LOAD_FAST 0 (x)
3 LOAD_CONST 4 ((1, 2, 3))
6 COMPARE_OP 6 (in)
9 RETURN_VALUE
注意(1, 2, 3)
常量不需要逐项构建 - 这是因为它已经由编译器构建并插入到函数的环境中。因此,is_valid
的实施可能实际上比使用集合的更快!这很容易测试:
$ python -m timeit -s 'def is_valid(x): return x in {1, 2, 3}' 'is_valid(-1)'
10000000 loops, best of 3: 0.189 usec per loop
$ python -m timeit -s 'def is_valid(x): return x in (1, 2, 3)' 'is_valid(-1)'
10000000 loops, best of 3: 0.128 usec per loop
同样,另一位评论者是对的。
增加集合/元组的大小并不会使平衡倾向于有利于集合 - 构造一组n
项目然后执行一个快速的快速恒定时间搜索总是更昂贵,而不是只是迭代预先创建的元组寻找一个项目。这是因为集合创建必须分配集合(可能多次)并计算所有项目的散列。虽然元组搜索和集合大小都是O(n),但是集合的一个具有更大的常数因子。
实现O(1)查找的正确方法需要手动实现编译器为元组自动执行的优化:
_valid = {1, 2, 3}
def is_valid(x):
return x in _valid
使用元组将此代码与等效代码进行比较,即使项目数量较少,该集合也总是更快。随着项目数量的增长,设置成为O(1)查找的明显赢家。
答案 2 :(得分:1)
set的大小写已在python 3.5中改进(可能更早)。这是我的测试功能:
def is_valid(x):
return x in ('name', 'known_as')
生成的代码是:
2 0 LOAD_FAST 0 (x)
3 LOAD_CONST 3 (('name', 'known_as'))
6 COMPARE_OP 6 (in)
9 RETURN_VALUE
将元组更改为一组会生成以下代码:
2 0 LOAD_FAST 0 (x) 3 LOAD_CONST 3 (frozenset({'name', 'known_as'})) 6 COMPARE_OP 6 (in) 9 RETURN_VALUE
Python 3.7和3.8在两种情况下都生成相同的代码。计时结果为:
$ python3.5 -m timeit -s "def is_valid(x): return x in {'name', 'known_as'}" "is_valid('')" 10000000 loops, best of 3: 0.0815 usec per loop $ python3.5 -m timeit -s "def is_valid(x): return x in ('name', 'known_as')" "is_valid('')" 10000000 loops, best of 3: 0.0997 usec per loop
在元组情况下,将“名称”传递给is_valid的每个循环以0.0921微秒的速度运行,但仍比设置慢。