cv.cm

編碼 · 資料結構

堆疊與佇列教學:LIFO 與 FIFO 的差別與 Python 範例

閱讀約 6 分鐘

簡答

堆疊是後加入的項目最先被移除的集合(LIFO,後進先出),就像一疊盤子。佇列則是先加入的項目最先被移除的集合(FIFO,先進先出),就像結帳排隊的人龍。兩者的新增與移除都是 O(1) 時間。在 Python 中,用 list 當堆疊,用 collections.deque 當佇列。

堆疊與佇列解決什麼問題

許多程式需要先記住一些項目,稍後再依固定順序處理。堆疊先給你最新的項目,適合處理巢狀的工作:函式呼叫、復原歷史、括號配對與深度優先搜尋。佇列先給你最舊的項目,適合公平的等待順序:列印工作、工作排程器、訊息佇列與廣度優先搜尋。

兩者都是抽象資料型別(abstract data type):由它支援的操作來定義,而不是由儲存方式決定。你可以用陣列或鏈結串列來實作任何一種。

操作與名稱

操作堆疊(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

經典堆疊問題:括號配對

要檢查每個括號是否都以正確順序閉合,只要把每個左括號 push 進去。遇到右括號時,堆疊頂端必須是與它配對的左括號。

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)

均攤(amortized)的意思是:偶爾某次 append 會調整底層陣列的大小,成本為 O(n),但平均分攤到大量操作上,每次仍是 O(1)。

變體與常見錯誤

  • 雙端佇列(deque):兩端都能新增與移除。collections.deque 就是一種,它既能當堆疊也能當佇列。
  • 優先佇列(priority queue):先移除最小(或最大)的項目,而不是最舊的。請使用 heapq,詳見二元堆積那一頁。
  • 環形緩衝區(circular buffer):以環繞索引存放在陣列中的固定大小佇列。deque(maxlen=n) 在滿了之後會丟掉最舊的項目。
  • 執行緒安全佇列:queue.Queue 用於執行緒,asyncio.Queue 用於協程,等待項目時會阻塞。
  • 用兩個堆疊做出佇列:push 到輸入堆疊,當輸出堆疊為空時,再把全部項目倒過去。每個項目只會搬移一次,所以操作為均攤 O(1)。

常見問題

堆疊和佇列有什麼差別?

堆疊先移除最近加入的項目(LIFO),佇列先移除最舊的項目(FIFO)。兩者的新增與移除都是 O(1) 時間。

在 Python 中,佇列該用 list 還是 deque?

請用 collections.deque。用 pop(0) 從 list 前端移除是 O(n),因為其他元素都要搬移;deque.popleft() 則是 O(1)。

實際的程式在哪裡會用到堆疊?

儲存函式呼叫的呼叫堆疊、復原與重做歷史、瀏覽器的上一頁按鈕、運算式解析、括號配對,以及深度優先搜尋。

什麼是堆疊溢位(stack overflow)?

當呼叫堆疊的空間用完時就會發生,通常是因為遞迴太深或無限遞迴。在 Python 中,超過遞迴上限時會出現 RecursionError。

queue.Queue 和 collections.deque 一樣嗎?

不一樣。queue.Queue 是執行緒安全的佇列,帶有鎖,其 get 與 put 會阻塞,用來在執行緒之間傳遞工作。collections.deque 則是更快、不會阻塞的通用容器。

全部編碼教學