ZetCode

Python deque (双端队列)

最后修改于 2025 年 4 月 2 日

deque(发音为“deck”)是 Python collections 模块中的一个双端队列,它支持在两端进行高效的添加和弹出操作。与列表相比,这些操作的时间复杂度为 O(1)。本指南涵盖了 deque 的创建、操作、性能特征和实际应用。Deques 是队列、栈和滑动窗口算法的理想选择。

基本 deque 操作

Deques 支持两端的多样化操作,并具有一致的性能。它们可以为空初始化,也可以从可迭代对象初始化,并带有可选的最大长度。本示例演示了核心的 deque 操作。理解这些基本知识是有效利用 deques 的关键。

basic_operations.py
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 的性能进行了基准测试。了解这些差异有助于选择正确的数据结构。

performance.py
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 是这些模式的理想选择。

queues.py
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 有效地维护了窗口的不变量。

sliding_window.py
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 操作是原子的,但复合操作需要适当的锁定。

threading.py
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 与替代方案进行了比较。选择正确的数据结构取决于您的访问模式和性能需求。

comparison.py
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.QueueLifoQueue 是线程安全的,但在单线程使用时比 deque 慢。

使用 maxlen 时,Deques 可以用作循环缓冲区,自动丢弃旧元素。对于线程安全场景,请使用 queue 模块类;否则,在单线程端操作方面,deque 通常提供更好的性能。

最佳实践

当需要频繁地在两端添加/删除元素时,请使用 deques 而不是列表。对于线程安全,请添加适当的同步或使用 queue 模块。为有界集合设置 maxlen 以防止无界增长。对于队列/栈操作,请优先使用 deque 的内置方法,而不是手动操作列表。为滑动窗口算法和类似模式考虑使用 deque。

资料来源

从以下资源了解更多信息:Python deque 文档Python 时间复杂度Real Python deque 指南

作者

我叫 Jan Bodnar,是一名充满热情的程序员,拥有丰富的编程经验。我自 2007 年开始撰写编程文章。迄今为止,我已撰写了 1400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。

列出所有 Python 教程