【问题标题】:How Python execute [list] * num? what's time complexity and memory complexity?Python如何执行[list] * num?什么是时间复杂度和内存复杂度?
【发布时间】:2018-06-16 04:47:20
【问题描述】:

我是 Python 新手。我最近对语法“[list] * k”感到困惑。我想了解 Python 是如何实际执行它的。 示例:

>>> l = [1, 2] * 10
>>> l
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2]

假设 len(list) = n,当 Python 解释它时,我以我有限的知识有以下猜测。

  1. 它使用 list.extend(list) 方法。 因此它将占用 O(n * k) 空间并使用 O(n * k) 时间。

  2. 它只复制原始列表的引用并复制它的 k 个副本。 因此它将占用 O(k) 空间并使用 O(k) 时间。

如果我的第二个猜测是这样的话,下面的陈述为什么以及如何工作?

>>> l[3] = 100
>>> l
[1, 2, 1, 100, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2]

强烈欢迎任何官方设计文档、源代码和 PEP 参考!

跟进,

@JoshLee 和@MSeifert 提供的源码链接对理解内部机制很有帮助。请参阅list_repeat

以下代码sn-p确认空间复杂度为O(n * k)。

size = Py_SIZE(a) * n;

以下代码 sn-p 确认时间复杂度为 O(n * k)。

p = np->ob_item;
items = a->ob_item;
for (i = 0; i < n; i++) {
    for (j = 0; j < Py_SIZE(a); j++) {
        *p = items[j];
        Py_INCREF(*p);
        p++;
    }
}

【问题讨论】:

  • 你的第一个猜测是对的。
  • 如果您的第二个猜测是正确的,那么 l[3]=100 不可能工作,除非引用被懒惰地转换为副本。 (请记住,Python 列表不存储实际项目,它存储对项目的引用数组)。请注意,虽然时间复杂度为 O(nk),但复制是以 C 速度进行的,因此它比使用简单的 Python for 循环一次复制一项要快得多。
  • (cont) 这种速度差异是如此之大,以至于对于小型列表(例如,在几百个项目以下),您可以有效地将简单的复制和扩展操作视为原子 O(1) 操作使用显式 Python 循环一一执行此类操作。有关此主题的更多信息以及计时数据,请参阅 here
  • 这是list_repeat的cpython代码。

标签: python algorithm list syntax


【解决方案1】:

列表是指向 Python 对象的指针数组的浅包装。所以一个包含 1, 2, 3 的 Python 列表看起来像这样:

l = [1, 2, 3]

如果将它乘以 5,索引仍然会引用相同的项目,例如索引 0、3、6、9、12 将存储相同的内存地址(这解释了为什么更改一项不会全部更改) :

l2 = l * 5

但是,当列表指向的对象是可变时,对其中一个项目的更改(与您的示例中的替换相反)将反映在所有项目中:

>>> ls = [{1}, {2}, {3}]
>>> ls2 = ls*3
>>> ls[0].add(2)
>>> ls2
[{1, 2}, {2}, {3}, {1, 2}, {2}, {3}, {1, 2}, {2}, {3}]

因此,虽然这具有O(n*k) 的空间和时间复杂度,但它并没有创建一个包含指向不同对象的指针的列表那么糟糕:

[int(i % 3 + 1) for i in range(15)]

请注意,CPython 实际上重用了小整数——因此在处理像 1、2、3、...这样的整数时,最后的图像是不准确的。它就像第二个图像——但对于其他类,它们(还有更多例外)将是不同的对象。然而,这只影响因子,因此无论如何都会在 O 符号中消失,但很高兴知道。

列表乘法代码是用 C 编写的(就像许多内置函数和类型一样),你可以看到它here (CPython 3.6.4)

static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
    Py_ssize_t i, j;
    Py_ssize_t size;
    PyListObject *np;
    PyObject **p, **items;
    PyObject *elem;
    if (n < 0)
        n = 0;
    if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n)
        return PyErr_NoMemory();

    /* Create an empty list: Space complexity: k * n */
    size = Py_SIZE(a) * n;
    if (size == 0)
        return PyList_New(0);
    np = (PyListObject *) PyList_New(size);
    if (np == NULL)
        return NULL;

    items = np->ob_item;

    /* Special case: The multiplied list contains one item. Time complexity: k */
    if (Py_SIZE(a) == 1) {
        elem = a->ob_item[0];
        for (i = 0; i < n; i++) {
            items[i] = elem;
            Py_INCREF(elem);
        }
        return (PyObject *) np;
    }

    /* General case: The multiplied list contains more than one item: Time complexity: n * k */
    p = np->ob_item;
    items = a->ob_item;
    for (i = 0; i < n; i++) {
        for (j = 0; j < Py_SIZE(a); j++) {
            *p = items[j];
            Py_INCREF(*p);
            p++;
        }
    }
    return (PyObject *) np;
}

cmets 是我添加的,只是为了解释(未记录的)源代码。

【讨论】:

  • 很好的解释!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-08-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-20
  • 2016-02-13
  • 2013-09-12
相关资源
最近更新 更多