python set vs tuple lookup。在Tuple O(1)中查找?

时间:2017-07-10 16:58:20

标签: python

最近我在python here

上看到了一个问题

1Python 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)的论点进行单独的反思。所以我要问它在这里。

3 个答案:

答案 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)元素比较,其中nlen(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微秒的速度运行,但仍比设置慢。