繁体   English   中英

Python如何如此迅速地从列表中删除元素?

[英]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)]

这里发生了三件事:

  1. 正在创建100000000个int对象。 在CPython中创建对象需要分配内存并将内容放入该内存,这需要时间。
  2. 您正在运行一个循环。 这会极大地影响性能: [i for i in range(...)]list(range(...))慢得多。
  3. 大列表正在动态创建。

阅读您的问题,似乎您只是在考虑最后一点,而忽略了其他问题。 因此,您的时间安排不准确:创建大型列表不会花费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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM