Python deque (双端队列)
最后修改于 2025 年 4 月 2 日
deque(发音为“deck”)是 Python collections 模块中的一个双端队列,它支持在两端进行高效的添加和弹出操作。与列表相比,这些操作的时间复杂度为 O(1)。本指南涵盖了 deque 的创建、操作、性能特征和实际应用。Deques 是队列、栈和滑动窗口算法的理想选择。
基本 deque 操作
Deques 支持两端的多样化操作,并具有一致的性能。它们可以为空初始化,也可以从可迭代对象初始化,并带有可选的最大长度。本示例演示了核心的 deque 操作。理解这些基本知识是有效利用 deques 的关键。
from collections import deque
# Create a deque
d = deque([1, 2, 3]) # Initialize with elements
print(f"Initial deque: {d}") # deque([1, 2, 3])
# Append elements
d.append(4) # Add to right end
d.appendleft(0) # Add to left end
print(f"After appends: {d}") # deque([0, 1, 2, 3, 4])
# Pop elements
right = d.pop() # Remove from right
left = d.popleft() # Remove from left
print(f"After pops: {d}, popped: {left}, {right}") # deque([1, 2, 3]), 0, 4
# Additional example: Max length
limited = deque(maxlen=3)
for i in range(5):
limited.append(i)
print(limited) # Last 3 elements: deque([2, 3, 4], maxlen=3)
# Additional example: Rotation
d = deque([1, 2, 3, 4, 5])
d.rotate(2) # Right rotation
print(d) # deque([4, 5, 1, 2, 3])
d.rotate(-1) # Left rotation
print(d) # deque([5, 1, 2, 3, 4])
该示例展示了基本的 deque 创建和修改。append/appendleft 用于添加元素,而 pop/popleft 用于移除元素。maxlen 参数创建了一个有界 deque,当 deque 满时会丢弃旧元素。
旋转操作可以循环移动元素——正数向右旋转,负数向左旋转。与列表不同,deque 在两端的插入和删除操作的效率与大小无关。这使得 deques 成为 FIFO/LIFO 结构的理想选择。
性能特征
与列表的 O(n) 复杂度相比,Deques 在两端的添加/弹出操作提供了 O(1) 的性能。本示例对 deque 和 list 的性能进行了基准测试。了解这些差异有助于选择正确的数据结构。
from collections import deque
import timeit
# Test data sizes
sizes = [1000, 10000, 100000]
print(f"{'Size':<10}{'deque append':<20}{'list append':<20}")
for size in sizes:
# Time deque appendleft
d_time = timeit.timeit(
'd.appendleft(0)',
setup=f'from collections import deque; d = deque(range({size}))',
number=10000
)
# Time list insert(0)
l_time = timeit.timeit(
'l.insert(0, 0)',
setup=f'l = list(range({size}))',
number=10000
)
print(f"{size:<10}{d_time:<20.5f}{l_time:<20.5f}")
# Additional example: Middle access
print("\nMiddle access performance:")
d = deque(range(100000))
l = list(range(100000))
%timeit d[50000] # Slower than list
%timeit l[50000] # Faster O(1) access
基准测试显示,deque 在左侧添加元素时具有一致的 O(1) 性能,而列表的 insert(0) 随着大小的增长会变慢至 O(n)。Deques 针对末端操作进行了优化,但在中间访问时性能为 O(n)。
列表在随机访问(lst[index])方面更快,因为 deques 是作为双向链表实现的。当需要频繁地在两端添加/删除元素时,选择 deques;当需要随机访问或固定长度操作时,选择列表。
队列实现
Deques 自然地实现了 FIFO(队列)和 LIFO(栈)结构。本示例展示了线程安全的队列实现。由于其高效的末端操作,Deques 是这些模式的理想选择。
from collections import deque
import threading
# FIFO Queue (First-In-First-Out)
class Queue:
def __init__(self):
self._items = deque()
self._lock = threading.Lock()
def enqueue(self, item):
with self._lock:
self._items.append(item)
def dequeue(self):
with self._lock:
return self._items.popleft()
def size(self):
with self._lock:
return len(self._items)
# LIFO Stack (Last-In-First-Out)
class Stack:
def __init__(self):
self._items = deque()
def push(self, item):
self._items.append(item)
def pop(self):
return self._items.pop()
def peek(self):
return self._items[-1] if self._items else None
# Usage
q = Queue()
q.enqueue('a')
q.enqueue('b')
print(q.dequeue()) # 'a'
s = Stack()
s.push('x')
s.push('y')
print(s.pop()) # 'y'
# Additional example: Bounded queue
class BoundedQueue:
def __init__(self, max_size):
self._items = deque(maxlen=max_size)
def enqueue(self, item):
if len(self._items) == self._items.maxlen:
raise Exception("Queue full")
self._items.append(item)
def dequeue(self):
return self._items.popleft()
bq = BoundedQueue(2)
bq.enqueue(1)
bq.enqueue(2)
# bq.enqueue(3) # Raises Exception
Queue 类使用 append(入队)和 popleft(出队)实现线程安全的 FIFO 操作。Stack 使用 append/pop 来实现 LIFO 行为。两者都利用了 deque 高效的末端操作。
BoundedQueue 展示了如何使用 maxlen 来创建固定大小的队列。当 deque 满时,它会自动丢弃旧元素,但本示例强制抛出异常。这些模式在生产者-消费者场景中很常见。
滑动窗口算法
Deques 在滑动窗口问题上表现出色,在这些问题中,您需要在序列上维护一个元素窗口。本示例演示了一个最大滑动窗口实现。Deques 有效地维护了窗口的不变量。
from collections import deque
def max_sliding_window(nums, k):
"""Return max of each sliding window of size k."""
q = deque()
result = []
for i, num in enumerate(nums):
# Remove indices outside current window
while q and q[0] <= i - k:
q.popleft()
# Remove smaller elements from back
while q and nums[q[-1]] <= num:
q.pop()
q.append(i)
# Window is fully formed
if i >= k - 1:
result.append(nums[q[0]])
return result
# Example usage
nums = [1, 3, -1, -3, 5, 3, 6, 7]
k = 3
print(max_sliding_window(nums, k)) # [3, 3, 5, 5, 6, 7]
# Additional example: Moving average
def moving_average(stream, window_size):
q = deque(maxlen=window_size)
for num in stream:
q.append(num)
yield sum(q) / len(q)
print(list(moving_average([10, 20, 30, 40, 50], 3))) # [10.0, 15.0, 20.0, 30.0, 40.0]
max_sliding_window 函数在 deque q 中维护索引,使得 nums[q[0]] 始终是当前窗口中的最大值。窗口外的元素会从前面移除,而比新索引小的元素会在添加新索引之前从后面移除。
moving_average 生成器展示了一个更简单的滑动窗口应用,使用 maxlen 来维护窗口大小。与手动操作列表相比,Deques 使这些算法更加简洁高效。
多线程与 deque
在适当同步的情况下,Deques 可以在多线程应用程序中使用。本示例展示了一个线程安全的生产者-消费者模式。虽然 deque 操作是原子的,但复合操作需要适当的锁定。
from collections import deque
import threading
import time
import random
class ProducerConsumer:
def __init__(self, max_size):
self.buffer = deque(maxlen=max_size)
self.lock = threading.Lock()
self.not_empty = threading.Condition(self.lock)
self.not_full = threading.Condition(self.lock)
def produce(self, item):
with self.not_full:
while len(self.buffer) == self.buffer.maxlen:
self.not_full.wait()
self.buffer.append(item)
self.not_empty.notify()
def consume(self):
with self.not_empty:
while not self.buffer:
self.not_empty.wait()
item = self.buffer.popleft()
self.not_full.notify()
return item
# Test
def producer(pc, items):
for item in items:
time.sleep(random.random())
pc.produce(item)
print(f"Produced: {item}")
def consumer(pc, count):
for _ in range(count):
time.sleep(random.random() * 2)
item = pc.consume()
print(f"Consumed: {item}")
pc = ProducerConsumer(3)
producer_thread = threading.Thread(
target=producer, args=(pc, ['A', 'B', 'C', 'D', 'E'])
)
consumer_thread = threading.Thread(
target=consumer, args=(pc, 5)
)
producer_thread.start()
consumer_thread.start()
producer_thread.join()
consumer_thread.join()
ProducerConsumer 类使用有界 deque 和条件变量来协调生产者和消费者。生产者在缓冲区满时等待,消费者在缓冲区空时等待。通知确保了在条件变化时线程能够被唤醒。
虽然 deque 操作对于单个操作是线程安全的,但复合操作(如“检查-然后执行”)需要同步。该示例展示了用于线程协调的适当锁定,以防止多线程场景中的竞态条件。
Deque 与其他数据结构
Deques 与列表、队列和其他集合相比,具有不同的权衡。本示例将 deques 与替代方案进行了比较。选择正确的数据结构取决于您的访问模式和性能需求。
from collections import deque
from queue import Queue, LifoQueue
import timeit
# Deque vs List for queue operations
print("Queue operations (popleft vs pop(0)):")
d = deque(range(100000))
l = list(range(100000))
%timeit d.popleft() # Much faster
%timeit l.pop(0) # Slow for large lists
# Deque vs queue.Queue
print("\nqueue.Queue vs deque:")
q = Queue()
d = deque()
%timeit q.put(1); q.get() # Thread-safe but slower
%timeit d.append(1); d.popleft() # Faster but not thread-safe
# Deque vs LifoQueue for stacks
print("\nStack operations:")
s = LifoQueue()
d = deque()
%timeit s.put(1); s.get() # Thread-safe stack
%timeit d.append(1); d.pop() # Faster alternative
# Additional example: Deque as circular buffer
def circular_buffer():
d = deque(maxlen=10)
for i in range(15):
d.append(i)
return list(d) # Last 10 elements
print(circular_buffer()) # [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
在队列操作(popleft 对比 pop(0))方面,Deques 的性能优于列表,而在栈操作方面则与列表相当。queue.Queue 和 LifoQueue 是线程安全的,但在单线程使用时比 deque 慢。
使用 maxlen 时,Deques 可以用作循环缓冲区,自动丢弃旧元素。对于线程安全场景,请使用 queue 模块类;否则,在单线程端操作方面,deque 通常提供更好的性能。
最佳实践
当需要频繁地在两端添加/删除元素时,请使用 deques 而不是列表。对于线程安全,请添加适当的同步或使用 queue 模块。为有界集合设置 maxlen 以防止无界增长。对于队列/栈操作,请优先使用 deque 的内置方法,而不是手动操作列表。为滑动窗口算法和类似模式考虑使用 deque。
资料来源
从以下资源了解更多信息:Python deque 文档、Python 时间复杂度 和 Real Python deque 指南。
作者
我叫 Jan Bodnar,是一名充满热情的程序员,拥有丰富的编程经验。我自 2007 年开始撰写编程文章。迄今为止,我已撰写了 1400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。
列出所有 Python 教程。