cv.cm

编码 · 数据结构

栈和队列详解:用 Python 示例讲清 LIFO 与 FIFO

阅读约 6 分钟

简短回答

栈是一种后添加的元素最先被移除的集合(LIFO,后进先出),就像一摞盘子。队列是先添加的元素最先被移除的集合(FIFO,先进先出),就像结账时排的队。两者的添加和移除都是 O(1) 时间。在 Python 中,用 list 当栈,用 collections.deque 当队列。

栈和队列解决什么问题

许多程序需要先记住一些元素,之后再按固定顺序处理。栈先给出最近加入的元素,适合嵌套式的工作:函数调用、撤销历史、括号匹配和深度优先搜索。队列先给出最早加入的元素,适合公平的等待:打印任务、任务调度器、消息队列和广度优先搜索。

两者都是抽象数据类型:它们由操作定义,而不是由存储方式定义。你可以基于数组或链表来实现其中任何一个。

操作与名称

操作栈(LIFO)队列(FIFO)
添加元素push:放到栈顶enqueue:加到队尾
移除元素pop:从栈顶取出dequeue:从队头取出
查看但不移除peek / toppeek / front
Python 类型list,配合 append 和 popcollections.deque,配合 append 和 popleft
输入 1、2、3 时的移除顺序3, 2, 11, 2, 3

示例演练:逐步 push 和 pop

把 1、2、3 依次放入两种结构,然后移除两个元素。

步骤栈的内容(栈顶在右)队列的内容(队头在左)
添加 1[1][1]
添加 2[1, 2][1, 2]
添加 3[1, 2, 3][1, 2, 3]
移除返回 3,剩下 [1, 2]返回 1,剩下 [2, 3]
移除返回 2,剩下 [1]返回 2,剩下 [3]

Python 实现

Python 的 list 本身就是个不错的栈:append 和 pop() 都作用于列表末尾,均摊时间为 O(1)。做队列时不要用 list.pop(0),因为它会移动所有剩余元素,代价是 O(n)。请使用 collections.deque,它在两端都是 O(1)。

from collections import deque
stack = []
stack.append(1)
stack.append(2)
stack.append(3)
print(stack.pop(), stack[-1])   # 3 2
queue = deque()
queue.append("a")
queue.append("b")
queue.append("c")
print(queue.popleft(), queue[0])  # a b
print(len(stack), len(queue))     # 2 2

如果想要有名字的方法和清晰的报错,可以用一个小类把内置类型包装起来:

from collections import deque
class Queue:
    def __init__(self):
        self._items = deque()
    def enqueue(self, item):
        self._items.append(item)
    def dequeue(self):
        if not self._items:
            raise IndexError("dequeue from empty queue")
        return self._items.popleft()
    def __len__(self):
        return len(self._items)
q = Queue()
q.enqueue("job1")
q.enqueue("job2")
print(q.dequeue(), len(q))  # job1 1

经典栈问题:括号匹配

要检查每个括号是否按正确顺序闭合,就把每个左括号压入栈。遇到右括号时,栈顶必须是与之匹配的左括号。

def is_balanced(text):
    pairs = {")": "(", "]": "[", "}": "{"}
    stack = []
    for ch in text:
        if ch in "([{":
            stack.append(ch)
        elif ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
    return not stack
print(is_balanced("f(a[1], {b: 2})"))  # True
print(is_balanced("(]"))               # False
print(is_balanced("(("))               # False

同样的思路还能用来计算逆波兰表达式、把递归改写成循环,以及实现深度优先搜索:用你自己的列表代替调用栈。

经典队列问题:广度优先搜索

广度优先搜索按层访问图。队列恰好能做到这一点:先发现的节点先处理。

from collections import deque
def bfs_order(graph, start):
    seen = {start}
    queue = deque([start])
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for nxt in graph[node]:
            if nxt not in seen:
                seen.add(nxt)
                queue.append(nxt)
    return order
graph = {"A": ["B", "C"], "B": ["D"], "C": ["D"], "D": []}
print(bfs_order(graph, "A"))  # ['A', 'B', 'C', 'D']

时间与空间复杂度

操作栈(list)队列(deque)队列(list.pop(0))
添加均摊 O(1)O(1)均摊 O(1)
移除O(1)O(1)O(n),元素左移
查看O(1)O(1)O(1)
n 个元素的空间O(n)O(n)O(n)

均摊的意思是:偶尔一次 append 会重新分配底层数组,代价为 O(n),但在大量操作上取平均,每次操作都是 O(1)。

变体与常见错误

  • 双端队列(deque):两端都能添加和移除。collections.deque 就是其中一种,它既可以当栈用,也可以当队列用。
  • 优先队列:先移除最小(或最大)的元素,而不是最早的。使用 heapq;参见二叉堆页面。
  • 循环缓冲区:用数组存储、下标回绕的定长队列。deque(maxlen=n) 在满时会丢弃最旧的元素。
  • 线程安全队列:queue.Queue 用于线程,asyncio.Queue 用于协程,它们在等待元素时会阻塞。
  • 用两个栈实现队列:向入栈压入元素,当出栈为空时把所有元素倒过去。每个元素只移动一次,所以操作是均摊 O(1)。

常见问题

栈和队列有什么区别?

栈先移除最近添加的元素(LIFO),队列先移除最早添加的元素(FIFO)。两者的添加和移除都是 O(1) 时间。

在 Python 中做队列应该用 list 还是 deque?

用 collections.deque。用 pop(0) 从 list 头部移除是 O(n),因为其余元素都要移动;而 deque.popleft() 是 O(1)。

栈在实际程序中用在哪里?

存储函数调用的调用栈、撤销和重做历史、浏览器的后退按钮、表达式解析、括号匹配以及深度优先搜索。

什么是栈溢出?

当调用栈空间耗尽时就会发生,通常是因为递归过深或无限递归。在 Python 中,超过递归限制时表现为 RecursionError。

queue.Queue 和 collections.deque 一样吗?

不一样。queue.Queue 是带锁、get 和 put 可阻塞的线程安全队列,用于在线程之间传递任务。collections.deque 是更快的通用容器,不会阻塞。

全部编码教程