演算法圖鑑
Stack & Queue · 02 / 04

Queue & Deque佇列與雙端佇列

環狀陣列實作、雙端佇列

用在:工作排程、訊息佇列、BFS

時間複雜度O(1)
空間複雜度O(n)
難度入門
前置知識Array & Dynamic Array、Stack

01為什麼需要它

印表機、訊息佇列、工作排程

多個人同時送列印工作,先送的要先印。Kafka、RabbitMQ 這類系統把訊息排成一列,生產者從尾端放、消費者從前端拿。

為什麼用它佇列的「先進先出」就是公平的定義。每個人只能排到隊尾,服務永遠從隊頭開始,不會有人插隊。

BFS 為什麼一層一層走

圖的廣度優先搜尋要「先看完距離 1 的,再看距離 2 的」。這個順序怎麼保證?

為什麼用它把發現的節點依序放進佇列,永遠先處理最早發現的。先進先出自動維持了「離起點近的先處理」。BFS 那篇的示範就是佇列在動。

為什麼 Python 用 list 當佇列會很慢

有人用 list.pop(0) 出隊,資料一多程式就卡住。

為什麼用它陣列從前端移除要把所有元素往前搬,O(n)。環狀陣列讓 head 往前走而不搬元素,deque 就是這樣做的(實際上是分段的雙向串列)。知道原因,就知道該換 deque。

看到這些關鍵字就想到它:先進先出、排隊、公平處理、一層一層、BFS、生產者消費者、兩端都要操作。

02核心概念

佇列從一端進、另一端出:enqueue 放到尾端,dequeue 從前端拿走。先進先出(FIFO),最早放進去的最先被處理。堆疊記住「最近發生的事」,佇列記住「最早發生的事」,兩者是一組對照,也決定了 DFS 和 BFS 的差別。

用陣列做佇列有個陷阱:從前端移除要把後面的元素全部往前搬,O(n)。解法是環狀陣列:記住 headsize,出隊只把 head 往前移一格,尾端位置用 (head + size) % 容量 算,走到底就繞回 0。這樣兩端操作都是 O(1),而且記憶體連續、快取友善。用鏈結串列也可以,但每個節點多一個指標。

雙端佇列(deque)兩端都能進出。它同時是堆疊也是佇列,Python 的 collections.deque、C++ 的 std::deque 都是標準配備。下一章的單調佇列就建立在「尾端能彈出、前端也能彈出」這個能力上。

03演算法步驟

  1. 1需要先進先出時,Python 用 deque、C++ 用 std::queue。不要用 list 的 pop(0)
  2. 2自己實作固定容量佇列用環狀陣列:記 headsize(不要記 head 和 tail,會分不清空與滿)。
  3. 3入隊buf[(head + size) % cap] = x,size 加一。滿了就回傳失敗或擴容。
  4. 4出隊head = (head + 1) % cap,size 減一。不搬任何元素。
  5. 5BFS 的骨架:起點入隊;while queue:出隊一個、處理它、把還沒看過的鄰居入隊。要分層就在每一輪記下當時的佇列長度。

04互動示範

容量 6 的環狀陣列。enqueue 幾次再 dequeue 幾次,看 tail 怎麼繞回陣列前面、head 怎麼往前走而不搬任何元素。

底層陣列(size 0 / 6
[0]
tail
[1]
[2]
[3]
[4]
[5]

邏輯順序(前 → 後):

容量 6 的環狀陣列。head 指向最前面的元素,tail = (head + size) % 6 是下一個空位。

05程式碼

內建的 deque / queue 用法、環狀陣列實作、以及用兩個堆疊做出佇列(攤銷分析那篇的例子,這裡給完整程式碼)。

from collections import deque

# 用 deque,不要用 list:list.pop(0) 是 O(n)
q = deque()
q.append(1)          # 入隊(尾端)O(1)
q.append(2)
q.popleft()          # 出隊(前端)O(1) → 1
q[0]                 # 看最前面

# deque 兩端都能用,所以也能當堆疊或滑動視窗
d = deque([1, 2, 3])
d.appendleft(0)      # 前端加入
d.pop()              # 尾端移除


# 環狀陣列實作固定容量的佇列(LeetCode 622)
class CircularQueue:
    def __init__(self, k):
        self.buf = [None] * k
        self.cap = k
        self.head = 0        # 最前面的元素
        self.size = 0

    def enqueue(self, x):
        if self.size == self.cap:
            return False
        tail = (self.head + self.size) % self.cap   # 繞回去
        self.buf[tail] = x
        self.size += 1
        return True

    def dequeue(self):
        if self.size == 0:
            return False
        self.head = (self.head + 1) % self.cap      # 不搬元素,只移 head
        self.size -= 1
        return True

    def front(self):
        return -1 if self.size == 0 else self.buf[self.head]


# 用兩個堆疊做佇列(LeetCode 232):攤銷 O(1)
class QueueWithStacks:
    def __init__(self):
        self.inbox, self.outbox = [], []

    def push(self, x):
        self.inbox.append(x)

    def pop(self):
        if not self.outbox:                  # 只有 outbox 空了才倒
            while self.inbox:
                self.outbox.append(self.inbox.pop())
        return self.outbox.pop()

06練習題

  • LeetCode 232Implement Queue using StacksEasy
  • LeetCode 225Implement Stack using QueuesEasy
  • LeetCode 622Design Circular QueueMedium
  • LeetCode 933Number of Recent Calls(滑動時間窗)Easy
  • LeetCode 102Binary Tree Level Order Traversal(用佇列分層)Medium
  • LeetCode 641Design Circular DequeMedium