Python中的双端队列如何实现?何时比列表差?


84

最近,我开始研究如何在Python中实现各种数据结构,以使我的代码更高效。在研究列表和双端队列的工作方式时,我发现,我想转移和取消移位,可以将列表中的O(n)减少到双端队列的O(1)的时间(列表实现为具有每次在前面插入东西时都可以完全复制,等等。)。我似乎找不到关于如何实现双端队列的细节,以及其缺点与列表的细节。有人可以启发我解决这两个问题吗?

Answers:


73

https://github.com/python/cpython/blob/v3.8.1/Modules/_collectionsmodule.c

Adequeobjectblock节点的双向链接列表组成。

因此,deque是的,另一个答案表明,a是一个(双链接)列表。

详细说明:这意味着Python列表对于随机访问和固定长度的操作(包括切片)要好得多,而双端队列在将内容推入和弹出两端时更有用,因为索引(但不是切片)可能但比列表慢。


3
请注意,如果您只需要在一端(堆栈)追加并弹出,列表应该会更好,.append()并且应.pop()摊销O(1)(重新分配和复制会发生,但很少,并且只有在达到最大容量时才会出现)曾经有过)。

@delnan:但是,如果您要排队,那么deque肯定是正确的方法。
JAB

@delnan:你怎么看?.append()和.pop()在列表中摊销为O(1),但在双端队列中它们不是实际的O(1),因此永远不需要复制。
伊莱

1
@Eli:列表不涉及线程安全性(嗯,它并没有连接到其内部),并且很长一段时间以来,它们已经被许多聪明的人进行了调整。

3
@delnan:实际上,dequeCPython中的s也不真正处理线程安全。他们只是从使自己的操作原子化的GIL中受益(实际上,append并且pop从a的末尾list具有相同的保护)。在实践中,如果你只是使用堆栈,都listdeque在CPython的有效相同的性能; 块分配的频率更高deque(但不是普通链表的频率更高;每次在CPython实现中越过64个成员边界,您最终都只会分配/释放),但是缺少巨大的间歇副本可以弥补这一点。
ShadowRanger 2015年

51

结帐collections.deque。从文档:

双端队列从双端队列的任一侧支持线程安全,内存高效的追加和弹出,并且在任一方向上的性能都大致相同。

尽管列表对象支持类似的操作,但它们针对快速定长操作进行了优化,并且会为pop(0)和insert(0,v)操作产生O(n)内存移动成本,这会改变基础数据表示的大小和位置。

就像说的那样,使用pop(0)或insert(0,v)会对列表对象造成较大的损失。您不能在上使用切片/索引操作deque,但可以使用popleft/ appendleft,这deque是针对其进行了优化的操作。这是一个简单的基准来证明这一点:

import time
from collections import deque

num = 100000

def append(c):
    for i in range(num):
        c.append(i)

def appendleft(c):
    if isinstance(c, deque):
        for i in range(num):
            c.appendleft(i)
    else:
        for i in range(num):
            c.insert(0, i)
def pop(c):
    for i in range(num):
        c.pop()

def popleft(c):
    if isinstance(c, deque):
        for i in range(num):
            c.popleft()
    else:
        for i in range(num):
            c.pop(0)

for container in [deque, list]:
    for operation in [append, appendleft, pop, popleft]:
        c = container(range(num))
        start = time.time()
        operation(c)
        elapsed = time.time() - start
        print "Completed %s/%s in %.2f seconds: %.1f ops/sec" % (container.__name__, operation.__name__, elapsed, num / elapsed)

我的机器上的结果:

Completed deque/append in 0.02 seconds: 5582877.2 ops/sec
Completed deque/appendleft in 0.02 seconds: 6406549.7 ops/sec
Completed deque/pop in 0.01 seconds: 7146417.7 ops/sec
Completed deque/popleft in 0.01 seconds: 7271174.0 ops/sec
Completed list/append in 0.01 seconds: 6761407.6 ops/sec
Completed list/appendleft in 16.55 seconds: 6042.7 ops/sec
Completed list/pop in 0.02 seconds: 4394057.9 ops/sec
Completed list/popleft in 3.23 seconds: 30983.3 ops/sec

3
呵呵,只是注意到即使可以建立索引,也无法对双端队列进行切片。有趣。
JAB

1
计时为+1-有趣的是list附件比deque附件要快一些。
senderle 2011年

1
@zeekay:考虑到搜索特定项目的索引通常将需要遍历集合中的项目,并且您可以像索引adeque一样索引,这很奇怪list
JAB

1
@senderle:当然,list pops比s慢deque(可能是由于lists缩小时间歇性调整大小的成本较高,在这里deque只是将块释放回空闲列表或小型对象池),因此在选择数据结构时堆叠(又名LIFO队列),空到满到空性能长相稍好于deque(对于平均值6365K操作/秒append/ pop,对list的5578K操作/秒)。我怀疑deque在现实世界中会做得更好,因为deque的自由列表意味着首次增长要比收缩后增长更昂贵。
ShadowRanger

1
为了澄清我的自由列表参考:CPythondeque实际上不会free多达16个块(模块范围内,不是每个deque),而是将它们放在便宜的可用块阵列中以供重用。所以生长时deque,第一次,它总是从拉新块malloc(使append更贵),但如果它不断地为有点扩大,则有点萎缩,而且来来回回,它通常不涉及malloc/free时只要长度大致保持在1024个元素的范围内(空闲列表中16个块,每个块64个插槽)即可。
ShadowRanger

16

我怀疑,deque对象的文档条目阐明了您需要了解的大多数内容。引号:

双端队列从双端队列的任一侧支持线程安全,内存高效的附加和弹出操作,并且在任一方向上的性能大致相同。

但...

索引访问在两端均为O(1),但在中间降低为O(n)。为了快速随机访问,请改用列表。

我不得不看一下源代码,以了解该实现是链接列表还是其他内容,但是在我看来,它似乎deque具有与双向链接列表大致相同的特征。


10

除了所有其他有用的答案之外,这里还有一些其他信息,用于比较Python列表,双端队列,集合和词典上各种操作的时间复杂度(Big-Oh)。这应该有助于为特定问题选择正确的数据结构。


-3

虽然我不确定Python是如何实现的,但在这里我仅使用数组编写了Queues的实现。它的复杂度与Python的Queues相同。

class ArrayQueue:
""" Implements a queue data structure """

def __init__(self, capacity):
    """ Initialize the queue """

    self.data = [None] * capacity
    self.size = 0
    self.front = 0

def __len__(self):
    """ return the length of the queue """

    return self.size

def isEmpty(self):
    """ return True if the queue is Empty """

    return self.data == 0

def printQueue(self):
    """ Prints the queue """

    print self.data 

def first(self):
    """ Return the first element of the queue """

    if self.isEmpty():
        raise Empty("Queue is empty")
    else:
        return self.data[0]

def enqueue(self, e):
    """ Enqueues the element e in the queue """

    if self.size == len(self.data):
        self.resize(2 * len(self.data))
    avail = (self.front + self.size) % len(self.data) 
    self.data[avail] = e
    self.size += 1

def resize(self, num):
    """ Resize the queue """

    old = self.data
    self.data = [None] * num
    walk = self.front
    for k in range(self.size):
        self.data[k] = old[walk]
        walk = (1+walk)%len(old)
    self.front = 0

def dequeue(self):
    """ Removes and returns an element from the queue """

    if self.isEmpty():
        raise Empty("Queue is empty")
    answer = self.data[self.front]
    self.data[self.front] = None 
    self.front = (self.front + 1) % len(self.data)
    self.size -= 1
    return answer

class Empty(Exception):
""" Implements a new exception to be used when stacks are empty """

pass

在这里,您可以使用一些代码对其进行测试:

def main():
""" Tests the queue """ 

Q = ArrayQueue(5)
for i in range(10):
    Q.enqueue(i)
Q.printQueue()    
for i in range(10):
    Q.dequeue()
Q.printQueue()    


if __name__ == '__main__':
    main()

它不能像C实现那样快,但是它使用相同的逻辑。


1
不要重新发明轮子!
Abhijit Sarkar

问题是如何实现Python的双端队列。它不是在要求替代实施。
Gino Mempin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.