Python如何快速从列表中删除元素?

时间:2015-09-03 11:20:56

标签: python optimization time-complexity python-internals

我正在学习Python,我正在努力了解容器在实践中的运作方式。 有一个我无法解释的问题。 假设,我创建了非常大的列表:

>>> l = [i for i in range(100000000)] # ~3 sec

创建它需要大约3秒钟(我使用升序号而不是相同的值来避免可能的优化)

我们可以阅读here,删除操作费用O(n)。但是当我从列表中间删除一个元素时,它会立即返回(就像任何其他简单命令一样快,如元素访问)

>>> del l[50000000] # instantly (< 0.1 sec)

之后,我可以在删除后的不到3秒内访问元素l[25000000]l[75000000],并且它也会立即执行(因此,我无法通过延迟或删除背景来解释这一点)。

有人可以解释一下,内部是如何完成的?该列表实际上是作为某种树实现的吗?这听起来很奇怪,而且会违反constant time element access的要求。

它是一种常见的优化,例如C ++中的返回值优化,还是一种罕见的,仅针对我的平台/版本?

我使用Linux和Python 3.4.1(Python 2.7.9显示相同的结果)。

2 个答案:

答案 0 :(得分:5)

我决定将我的一组评论转化为正确的答案。

首先,让我们澄清一下你在做什么时发生的事情:

>>> l = [i for i in range(100000000)]

这里发生了三件事:

  1. 正在创建100000000个int个对象。在CPython中创建对象需要分配内存并将内容放入该内存中,这需要时间。
  2. 您正在运行循环。这会大大影响性能:[i for i in range(...)]list(range(...))慢得多。
  3. 正在动态创建大型列表。
  4. 阅读你的问题,似乎你只考虑最后一点,忽略其他人。因此,您的计时不准确:创建一个大型列表不需要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]不同,@button.buttonAndHandler(_('Save'), name='save') def handleAdd(self, action): data, errors = self.extractData() if errors: self.status = self.formErrorsMessage return obj = self.createAndAdd(data) if obj is not None: # mark only as finished if we get the new object self._finishedAdd = True IStatusMessage(self.request).addStatusMessage( self.success_message, "info" ) 只需移动指针的一半(前半部分的指针不需要移动)。 “移动一半的指针”等于“执行一半的操作”,这大致意味着“在一半时间内运行”(这并非总是如此,但时间表明在这种情况下这是正确的。)

答案 1 :(得分:3)

我不确定为什么你认为删除单个元素需要3秒钟。

您的初始时间是100000000个人追加操作。每个都需要几分之一秒;你的删除操作需要相似的时间。

无论如何,正如Bartosz指出的那样,O(n)复杂性并不意味着所有操作都花费相同的时间长度,这意味着时间长度与列表的长度成正比。