最近,我开始研究如何在Python中实现各种数据结构,以使我的代码更高效。在研究列表和双端队列的工作方式时,我发现,我想转移和取消移位,可以将列表中的O(n)减少到双端队列的O(1)的时间(列表实现为具有每次在前面插入东西时都可以完全复制,等等。)。我似乎找不到关于如何实现双端队列的细节,以及其缺点与列表的细节。有人可以启发我解决这两个问题吗?
Answers:
https://github.com/python/cpython/blob/v3.8.1/Modules/_collectionsmodule.c
A
dequeobject
由block
节点的双向链接列表组成。
因此,deque
是的,另一个答案表明,a是一个(双链接)列表。
详细说明:这意味着Python列表对于随机访问和固定长度的操作(包括切片)要好得多,而双端队列在将内容推入和弹出两端时更有用,因为索引(但不是切片)可能但比列表慢。
deque
肯定是正确的方法。
deque
CPython中的s也不真正处理线程安全。他们只是从使自己的操作原子化的GIL中受益(实际上,append
并且pop
从a的末尾list
具有相同的保护)。在实践中,如果你只是使用堆栈,都list
和deque
在CPython的有效相同的性能; 块分配的频率更高deque
(但不是普通链表的频率更高;每次在CPython实现中越过64个成员边界,您最终都只会分配/释放),但是缺少巨大的间歇副本可以弥补这一点。
结帐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
list
附件比deque
附件要快一些。
deque
一样索引,这很奇怪list
。
list
pop
s比s慢deque
(可能是由于list
s缩小时间歇性调整大小的成本较高,在这里deque
只是将块释放回空闲列表或小型对象池),因此在选择数据结构时堆叠(又名LIFO队列),空到满到空性能长相稍好于deque
(对于平均值6365K操作/秒append
/ pop
,对list
的5578K操作/秒)。我怀疑deque
在现实世界中会做得更好,因为deque
的自由列表意味着首次增长要比收缩后增长更昂贵。
deque
实际上不会free
多达16个块(模块范围内,不是每个deque
),而是将它们放在便宜的可用块阵列中以供重用。所以生长时deque
,第一次,它总是从拉新块malloc
(使append
更贵),但如果它不断地为有点扩大,则有点萎缩,而且来来回回,它通常不涉及malloc
/free
时只要长度大致保持在1024个元素的范围内(空闲列表中16个块,每个块64个插槽)即可。
虽然我不确定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实现那样快,但是它使用相同的逻辑。
.append()
并且应.pop()
摊销O(1)(重新分配和复制会发生,但很少,并且只有在达到最大容量时才会出现)曾经有过)。