为什么deque实现为链表而不是循环数组?

时间:2017-07-16 23:16:45

标签: python python-3.x cpython python-internals

CPython dequeimplemented,是一个包含64个项目大小的“块”(数组)的双向链表。除了链表两端的那些块外,这些块都是满的。 IIUC,当pop / popleft删除块中的最后一项时,块被释放;当append / appendleft尝试添加新项目并且相关块已满时,会分配它们。

我理解the listed advantages使用链接的列表列表而不是链接的项目列表:

  • 减少指向每个项目的prev和next的指针的内存成本
  • 为每个添加/删除的项目降低执行malloc / free的运行时成本
  • 通过将连续的指针放在彼此旁边来改善缓存局部性

但是为什么首先没有使用单个动态大小的圆形数组而不是双链表呢?

AFAICT,圆形阵列将保留所有上述优势,并在pop*保持append* / O(1)的(摊销)成本(通过分配,就像在{{1}中一样}})。此外,它还可以提高从当前listO(n)的索引查找成本。循环数组也可以更容易实现,因为它可以重用大部分O(1)实现。

我可以在C ++这样的语言中看到支持链表的论证,其中可以使用指针或迭代器在list中从中间删除项目。但是,python O(1)没有API来执行此操作。

2 个答案:

答案 0 :(得分:21)

改编自我在python-dev邮件列表上的回复:

双端队列的主要目的是使两端的弹出和推动变得有效。这就是当前实现的作用:每次推送或弹出的最坏情况常量时间,无论双端队列中有多少项目。那打败"摊销O(1)"在小和大。这就是为什么这样做的原因。

其他一些deque方法因此比列表慢,但是谁在乎呢?例如,我曾经用过deque的唯一索引是0和-1(偷看一个deque的一端或另一端),并且实现也使得访问这些特定索引的时间也是恒定的。

确实,来自Raymond Hettinger的消息由Jim Fasarakis Hilliard在评论中引用:

https://www.mail-archive.com/python-dev@python.org/msg25024.html

确认

  

__getitem__的唯一原因是支持快速访问头部和尾部而不实际弹出值

答案 1 :(得分:4)

除了接受@TimPeters答案之外,我还想添加一些不符合评论格式的其他观察结果。

让我们只关注一个常见的用例,其中removeRows()被用作一个简单的FIFO队列。

一旦队列达到其峰值大小,循环数组就不再需要从堆中分配内存。我认为这是一个优势,但事实证明CPython实现通过保留可重用内存块列表实现了相同的目的。打平。

当队列大小增加时,循环数组和CPython都需要来自堆的内存。 CPython需要一个简单的deque,而数组需要(可能更昂贵)malloc(除非在原始内存块的右边缘有额外的空间可用,它需要释放旧的记忆和复制数据)。 CPython的优势。

如果队列的大小比其稳定大小大得多,那么CPython和数组实现都会浪费未使用的内存(前者通过将其保存在可重用的阻止列表中,后者通过将未使用的空闲空间保留在数组)。打平。

正如@TimPeters指出的那样,每个FIFO队列put / get的成本对于CPython总是realloc,但只对该数组进行摊销O(1)。 CPython的优势。

相关问题