[英]How does Python remove elements from a list so quickly?
我现在正在研究Python,并且试图了解容器在实际中是如何工作的。 有一个我无法解释的问题。 假设我创建了一个很大的列表:
>>> l = [i for i in range(100000000)] # ~3 sec
创建它大约需要3秒钟(我使用升序数字而不是相同的值来避免可能的优化)
正如我们在这里可以看到的,删除操作成本为O(n)
。 但是,当我从列表的中间删除一个元素时,它会立即返回(就像其他任何简单命令一样快,例如元素访问)
>>> del l[50000000] # instantly (< 0.1 sec)
之后,我可以在删除后不到3秒的时间内访问元素l[25000000]
和l[75000000]
,并且它也可以立即执行(因此,我无法通过延迟删除或后台删除来解释这一点)。
有人可以解释一下,这是内部完成的吗? 列表实际上是作为某种树实现的吗? 这听起来很怪异,而且还违反了对恒定时间元素访问的要求。
它是常见的优化,例如C ++中的返回值优化,还是仅针对我的平台/版本的罕见的优化?
我使用Linux和Python 3.4.1(Python 2.7.9显示了相同的结果)。
我决定将我的评论集变成一个正确的答案。
首先,让我们澄清一下您所做的事情:
>>> l = [i for i in range(100000000)]
这里发生了三件事:
int
对象。 在CPython中创建对象需要分配内存并将内容放入该内存,这需要时间。 [i for i in range(...)]
比list(range(...))
慢得多。 阅读您的问题,似乎您只是在考虑最后一点,而忽略了其他问题。 因此,您的时间安排不准确:创建大型列表不会花费3秒,而只花费了这3秒的一小部分。
这个分数有多大是一个有趣的问题,仅使用Python代码很难回答,但我们仍然可以尝试。 具体来说,我会尝试以下语句:
>>> [None] * 100000000
在这里,CPython不必创建大量的对象(只有None
),不必运行循环,并且可以一次为列表分配内存(因为它事先知道了大小)。
时间是不言自明的:
$ python3 -m timeit "list(range(100000000))"
10 loops, best of 3: 2.26 sec per loop
$ python3 -m timeit "[None] * 100000000"
10 loops, best of 3: 375 msec per loop
现在,回到您的问题:删除项目怎么样?
$ python3 -m timeit --setup "l = [None] * 100000000" "del l[0]"
10 loops, best of 3: 89 msec per loop
$ python3 -m timeit --setup "l = [None] * 100000000" "del l[100000000 // 4]"
10 loops, best of 3: 66.5 msec per loop
$ python3 -m timeit --setup "l = [None] * 100000000" "del l[100000000 // 2]"
10 loops, best of 3: 45.3 msec per loop
这些数字告诉我们一些重要的事情。 请注意2×45.3≈89。还有66.5×4/3≈89。
这些数字准确地说明了线性复杂度是什么。 如果一个函数的时间复杂度为kn (即O(n) ),则意味着如果将输入加倍,则时间加倍; 如果我们将输入大小增加4/3,则时间将增加4/3。
这就是这里发生的事情。 在CPython中,我们的100000000项列表是一个连续的内存区域,其中包含指向Python对象的指针:
l = |ptr0|ptr1|ptr2|...|ptr99999999|
当我们运行del l[0]
我们将ptr1
从右移到左,覆盖ptr0
。 其他元素相同:
l = |ptr0|ptr1|ptr2|...|ptr99999999|
^^^^
` item to delete
l = |ptr1|ptr2|...|ptr99999999|
因此,当我们运行del l[0]
我们必须将99999998指针向左移动。 这与del l[100000000 // 2]
,后者仅需要移动一半的指针(前一半的指针不需要移动)。 “移动一半的指针”等于“执行一半的操作”,这大致意味着“在一半时间内运行”(这并不总是正确的,但根据计时表明,在这种情况下是正确的)。
我不确定为什么您认为删除单个元素需要3秒钟。
您的初始时间是用于100000000个单独的追加操作。 每一个都需要一秒钟的时间; 您的删除操作需要花费相似的时间。
在任何情况下,正如Bartosz所指出的那样,O(n)复杂度并不意味着所有操作都花费相同的时间长度,而是意味着时间长度与列表的长度成比例。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.