在 Python 中遍历 deque 的时间复杂度是多少?

What is the time complexity of iterating through a deque in Python?

提问人:perseverance 提问时间:11/17/2017 最后编辑:perseverance 更新时间:12/4/2021 访问量:2808

问:

迭代的时间复杂度是多少,或者更准确地说,每次迭代都来自 Python 的集合库?

一个例子是这样的:

elements = deque([1,2,3,4])
for element in elements:
  print(element)

每次迭代都是常数 O(1) 运算吗?或者它是否执行线性 O(n) 操作来获取每次迭代中的元素?

网上有许多关于时间复杂度的资源,包括所有其他 deque 方法,如 、 、 。似乎没有任何关于 deque 迭代的时间复杂度信息。appendleftappendpopleftpop

谢谢!

python-3.x python-collections

评论

1赞 Jean-François Fabre 11/17/2017
deque = 双端队列。所以找到第 n 个元素是O(n)
3赞 juanpa.arrivillaga 11/17/2017
是的。但是,如果你这样做,你可以得到线性迭代for element in elements:
3赞 juanpa.arrivillaga 11/17/2017
换言之,对 deque 的迭代是线性的,但重复索引到 deque 是二次的。
2赞 juanpa.arrivillaga 11/17/2017
@perseverance是的,如果你做索引,那就是必须发生的事情。幸运的是,如果你想迭代 deque,你不必这样做。你几乎永远不应该做.对于具有随机访问的对象来说,它只是丑陋且容易出错,但对于对象,您会感受到 O(N) 索引操作的痛苦。for i in range(len(elements))listdeque
1赞 user2357112 11/17/2017
@perseverance:如果你做对了,每次迭代将是 O(1),但你做错了。

答:

15赞 juanpa.arrivillaga 11/17/2017 #1

如果你的结构是这样的:

elements = deque([1,2,3,4])
for i in range(len(elements)):
    print(elements[i])

您不是在遍历 deque,而是在遍历对象,然后索引到 .这使得迭代多项式时间,因为每个索引操作都是 O(n)。然而,实际上迭代是线性时间。rangedequeelements[i]deque

for x in elements:
    print(x)

这是一个快速的实证测试:

import timeit
import pandas as pd
from collections import deque

def build_deque(n):
    return deque(range(n))

def iter_index(d):
    for i in range(len(d)):
        d[i]

def iter_it(d):
    for x in d:
        x

r = range(100, 10001, 100)

index_runs = [timeit.timeit('iter_index(d)', 'from __main__ import build_deque, iter_index, iter_it; d = build_deque({})'.format(n), number=1000) for n in r]
it_runs = [timeit.timeit('iter_it(d)', 'from __main__ import build_deque, iter_index, iter_it; d = build_deque({})'.format(n), number=1000) for n in r]

df = pd.DataFrame({'index':index_runs, 'iter':it_runs}, index=r)
df.plot()

以及生成的图:enter image description here

现在,我们实际上可以看到迭代器协议是如何为 CPython 源代码中的对象实现的:deque

首先,对象本身:deque

typedef struct BLOCK {
    struct BLOCK *leftlink;
    PyObject *data[BLOCKLEN];
    struct BLOCK *rightlink;
} block;

typedef struct {
    PyObject_VAR_HEAD
    block *leftblock;
    block *rightblock;
    Py_ssize_t leftindex;       /* 0 <= leftindex < BLOCKLEN */
    Py_ssize_t rightindex;      /* 0 <= rightindex < BLOCKLEN */
    size_t state;               /* incremented whenever the indices move */
    Py_ssize_t maxlen;
    PyObject *weakreflist;
} dequeobject;

因此,正如评论中所述,a 是“块”节点的双向链接列表,其中块本质上是 python 对象指针的数组。现在对于迭代器协议:deque

typedef struct {
    PyObject_HEAD
    block *b;
    Py_ssize_t index;
    dequeobject *deque;
    size_t state;          /* state when the iterator is created */
    Py_ssize_t counter;    /* number of items remaining for iteration */
} dequeiterobject;

static PyTypeObject dequeiter_type;

static PyObject *
deque_iter(dequeobject *deque)
{
    dequeiterobject *it;

    it = PyObject_GC_New(dequeiterobject, &dequeiter_type);
    if (it == NULL)
        return NULL;
    it->b = deque->leftblock;
    it->index = deque->leftindex;
    Py_INCREF(deque);
    it->deque = deque;
    it->state = deque->state;
    it->counter = Py_SIZE(deque);
    PyObject_GC_Track(it);
    return (PyObject *)it;
}

// ...

static PyObject *
dequeiter_next(dequeiterobject *it)
{
    PyObject *item;

    if (it->deque->state != it->state) {
        it->counter = 0;
        PyErr_SetString(PyExc_RuntimeError,
                        "deque mutated during iteration");
        return NULL;
    }
    if (it->counter == 0)
        return NULL;
    assert (!(it->b == it->deque->rightblock &&
              it->index > it->deque->rightindex));

    item = it->b->data[it->index];
    it->index++;
    it->counter--;
    if (it->index == BLOCKLEN && it->counter > 0) {
        CHECK_NOT_END(it->b->rightlink);
        it->b = it->b->rightlink;
        it->index = 0;
    }
    Py_INCREF(item);
    return item;
}

正如你所看到的,迭代器实质上是跟踪一个块索引、一个指向一个块的指针,以及一个 deque 中总项的计数器。如果计数器达到零,它就会停止迭代,如果没有,它会抓取当前索引处的元素,递增索引,递减计数器,并负责检查是否移动到下一个块。换句话说,在 Python 中,一个 deque 可以表示为列表列表,例如 ,它迭代d = [[1,2,3],[4,5,6]]

for block in d:
    for x in block:
        ...

评论

0赞 en_Knight 11/17/2017
到处都是很好的答案。不过,我预计该图是单调的——为什么它们在蓝色曲线上有很大的振荡?
0赞 juanpa.arrivillaga 11/17/2017
@en_Knight我只能推测来源,但可能主要是噪音。
0赞 en_Knight 11/17/2017
大概:)另外,我真的很喜欢引用实现,尽管这种行为不是特定于实现的可能毫无价值,但它也在规范中被引用
0赞 juanpa.arrivillaga 11/17/2017
@en_Knight我无法找到任何关于迭代的具体信息,但如果你删除一个链接,我会将其添加到答案中。