【考試向】資料結構筆記(堆疊、佇列、環狀佇列、後序表示式)
【考試向】資料結構筆記(堆疊、佇列、環狀佇列、後序表示式)
歡迎你點入本篇文章!我是 LukeTseng,本系列文章主要整理自學資料結構的一些知識,如果你喜歡我的文章,麻煩您不吝嗇的在文章底下按下一顆愛心,或是追蹤我唷~
最後更新時間:
堆疊(Stack)
特性:後進先出(LIFO:Last In First Out)
資料只能在頂端(Top)做堆入(Push),跟移除(Pop)。

Image Source:Stack Data Structure and Implementation in Python, Java and C/C++
應用:
- Ctrl + Z 復原功能
- 函式呼叫
- 括號匹配
- DFS(Depth First Search:深度優先搜尋)
時間複雜度:
- 堆入(Push): $O(1)$
- 移除(Pop): $O(1)$
優點:
- 記憶體管理高效:資料只在頂端操作,記憶體配置連續可預測。在系統層級(如 Call Stack),比堆積(Heap)快很多。
- 存取速度快($O(1)$):無論堆疊有多大,Push 跟 Pop 的時間複雜度永遠是 $O(1)$ 。
- 防資料碎片化:資料按順序堆疊,不會像其他結構一樣在記憶體中留下零散的空隙。
缺點:
- Stack Overflow:遞迴過深或存放過多,記憶體用太多易導致程式當掉。
- 靈活性差:不支援隨機存取(Random Access),要拿中間某個資料只能一個一個把東西拿出來才能拿到。
實作方式
使用 C 語言實作。
標頭檔 #include <stdbool.h> 用於 true/false。
- 變數
int top = -1用於追蹤 stack 目前的位置。 top == MAX - 1判斷 stack 滿了沒。top == -1判斷 stack 是否為空。
1 |
|
佇列(Queue)
特性:先進先出(FIFO:First In First Out)
想像陣列分為兩端開口:前端(Front)跟尾端(Rear)。
可以做到加入(enqueue)、移除(dequeue)元素這兩種操作。

Image Source:What is Queue Data Structure? - GeeksforGeeks
應用:
- 作業系統層面:
- CPU 工作排程:利用佇列管理待執行的程序(Processes),確保每個任務按順序或優先級獲得處理時間。

- IO 緩衝區(Buffering):當資料產生速度快於處理速度時(如印表機排隊列印、磁碟寫入等等),佇列可暫存資料避免遺失情形發生。
- BFS(Breadth First Search:廣度優先搜尋)
時間複雜度:
- Enqueue:$O(1)$
- Dequeue:$O(1)$(不移動元素,只移動指標)
優點:
- 保證公平性與順序:處理多執行緒任務排程或網路封包時很重要。
- 解耦(Decoupling):在分散式系統中,佇列(如 RabbitMQ, Kafka)可作為生產者與消費者之間的緩衝,讓雙方不需要同時在線,增加系統穩定性。
缺點:
- 假溢位問題:若用簡單陣列實作,當前端資料 dequeue 後,後面的資料若不往前移,就會浪費前端的記憶體空間。而一旦往前移的話,就會額外耗費 $O(N)$ 時間。
- 搜尋效率低:若要確認某個特定資料是否在佇列中,必須遍歷整個佇列 ($O(n)$)。
實作方式
使用 C 語言實作。
輸出結果顯示一般佇列實作方式的問題,移除資料時,剩餘資料沒有往前移,因此導致後面要加入資料進去就加不進去,這情況差不多就像是下圖那樣:

Image Source:作者繪製。
程式實作講解:
- 雙指標法:變數
int front = -1、int rear = -1各別追蹤前端、尾端。- enqueue 時
rear增加。 - dequeue 時
front增加。
- enqueue 時
rear == MAX - 1判別一個佇列是否已滿。front == -1 && rear == -1判別一個佇列是否為空。- 在
dequeue時做front >= rear判別,把兩個變數初始化成-1,免得front越加越多。
1 |
|
Output:
1 | 加入資料 10 |
環狀佇列(Circular Queue)
環狀佇列是為了解決一般佇列的假溢位問題而誕生的,將陣列的頭尾邏輯上相連,讓 rear 可以繞回 Index 0 繼續存放資料。
環狀佇列在物理上仍然是一個線性陣列,但在邏輯上,用模運算(Modulo Operator, %)讓索引值循環。
假設 front 跟 rear 兩指標為 index,則要做指標移動時,搭配模運算動:$$index = (index + 1) % N$$
當 $index$ 為 $N-1$(最後一格)時,$(N-1+1) % N$ 會變成 $0$。
實作方式 A:空位版
這是第一種實作方式,有一個缺點,就是會有一個空位留在那邊。
程式實作講解:
- 雙指標
int front = 0、int rear = 0,注意這邊要設成0而不是-1。front代表指向第一個元素rear代表指向下一個空位
front == rear判斷佇列是否為空。(rear + 1) % MAX == front判斷佇列是否滿了。因為這裡,才會把空位留在那不能加入。
1 |
|
Output:
1 | 加入資料 10 |
實作方式 B:無空位版
改進方式是在原有的程式碼中,新增 int count = 0;,去判斷目前有多少個元素,剩下邏輯不變。
基本上就是判斷目前佇列大小。
count == 0;判斷佇列是否為空。count == MAX;判斷佇列是否滿了。
1 |
|
Output:
1 | 加入資料 10 |
堆疊應用:後序表示式(Postfix notation)
在談後序表示式前,需要先知道中序表示式。
中序表示式(Infix Notation)
中序表示式是人類最習慣用的寫法,就是將兩個運算元之間夾帶一個運算子,如:$$1 + 1$$
這種形式的寫法就叫做中序表示式。
格式: $Operand \ A \quad Operator \quad Operand \ B$
缺點: 對於電腦來說,這個處理起來比較複雜,因為需要處理「運算子優先順序」(如乘除先於加減)以及「括號」來改變順序。如果用這個寫法來做處理,時間複雜度可能最多會到 $O(N^2)$ ,善用演算法技巧如雙堆疊法,可減至 $O(N)$ 。
後序表示式(Postfix notation)
後序表示式又稱為逆波蘭表示式(Reverse Polish Notation, RPN)。
這種寫法是可以讓電腦看得懂的寫法,而且會比較快,原理是將運算子放在兩個運算元後面,如:$$1 \ 1 \ +$$
原本中序表示式的寫法是 $$1 + 1$$
優點:
- 不需要括號
- 電腦好處理:電腦只要從左讀到右,遇到數字就存起來,遇到運算子就抓最近的兩個數字運算,非常適合以堆疊(Stack)來實作。
時間複雜度: $O(N)$ ,但是因為用堆疊實作,一些操作基本上是常數時間,執行速度上來說會比中序還快。
中序轉後序(堆疊法)
用堆疊將中序轉後序,主要依照以下規則:
- 遇到運算元(數字 or 變數):直接輸出。
- 遇到左括號
(:放入堆疊。 - 遇到右括號
):將堆疊中的運算子依序 pop 並輸出,直到 pop 左括號(為止(括號不輸出)。 - 遇到運算子(
+,-,*,/):- 檢查堆疊頂端的運算子。
- 若堆疊頂端運算子優先權 $\ge$ 目前運算子:將堆疊頂端 pop 並輸出,重複此步驟直到堆疊頂端優先權較小或是遇到
(。 - 將目前的運算子放入堆疊。
- 讀取完畢:將堆疊中剩餘的所有運算子依序 pop 並輸出。
至於優先順序,大致上就如下表所示:
| 優先順序(Priority) | 運算子(Operator) |
|---|---|
| 5(最高) | *, /, % |
| 4 | +, - |
| 3 | <, <=, >, >= |
| 2 | && |
| 1(最低) | ` |
另外在實作「中序轉後序」演算法時,需要兩數值來決定動作,因為左括號 ( 在堆疊外和堆疊內的優先順序是完全不同的。
因此在這邊會有兩個名詞:
- ICP(Incoming Priority):運算子在堆疊外(剛讀到,準備進去)的優先順序。
- ISP(In-Stack Priority):運算子在堆疊內(已在內)的優先順序。
運算規則:
- 若 $ICP > ISP \rightarrow$ 堆入堆疊(push)。
- 若 $ICP \le ISP \rightarrow$ pop 並輸出,直到 $ICP > ISP$ 為止,然後再 push。
優先順序表(數字越低,優先順序越低;數字越高,優先順序越高):
| 運算子(Operator) | ICP(堆疊外優先順序) | ISP(堆疊內優先順序) |
|---|---|---|
) | 1 | N/A |
*, /, % | 2 | 2 |
+, - | 1 | 1 |
( | 4 | 1 |
右括號 ) 被讀到後需要 pop 找到 ( 才結束,且不進堆疊。
左括號 ( 在進堆疊後,優先順序變最低的原因是為了不擋住後續進來的任何 +, -, *, /,讓括號內的運算子能順利堆疊起來。
中序轉後序(堆疊法)範例
將以下中序表示式轉換成後序表示式:$$A + B * (C - D) / E$$
解法:
先求出後序表示式的結果:$$A B C D - * E / +$$
ok 之後就開始來做模擬表了(記得規則就是上節的那五條):
| 讀取字元(next token) | 堆疊內容(stack) | 輸出(output) | 說明 |
|---|---|---|---|
| A | empty | A | 遇到運算元直接輸出 |
+ | + | A | 堆疊為空,直接 push |
| B | + | A B | 遇到運算元直接輸出 |
* | + * | A B | * 優先順序大於堆疊頂端 +,push |
( | + * ( | A B | 左括號 ( 直接 push |
C | + * ( | A B C | 遇到運算元直接輸出 |
- | + * ( - | A B C | 堆疊頂端是 (,直接 Push |
| D | + * ( - | A B C D | 遇到運算元直接輸出 |
) | + * | A B C D - | 遇到右括號,不斷 pop 直到遇到 (,由於這邊 - 被 pop 掉了,所以直接輸出 - |
/ | + / | A B C D - * | 1. 比較 * 與 /。2. * 優先順序 $\ge$ /,pop *。3. 接著比較 +,+ $\le$ /,因此 / push 進去。 |
| E | + / | A B C D - * E | 遇到運算元直接輸出 |
| none | empty | A B C D - * E / + | 字串讀完後,pop 所有剩餘項目(/ 接下來是 +) |
最後得到後序表示式就是 $A B C D - * E / +$ 。
中序轉後序(括號法)
繼續以上節的範例為例子: $$A + B * (C - D) / E$$
先找到原本的括號 $(C - D)$ ,就不動他,再看到運算子優先級最高的部分,也就是乘除的地方,由於優先級相同,於是先從左邊開始算: $$B * (C - D)$$
加上括號後,接著再算 $$(B * (C - D)) / E$$
一樣加上括號後,最後做加法 $$A + ((B * (C - D)) / E)$$
最後補齊括號 $$(A + ((B * (C - D)) / E))$$
做好前處理之後,就要先從最內層的括號開始轉換,每一組 (X op Y),轉成後序就是 X Y op。
第一個是 $$(C - D) \rightarrow C D -$$
接下來 $$(B * (C D -)) \rightarrow BCD - *$$
接下來 $$((B C D - *) / E) \rightarrow BCD - * E /$$
最後 $$(A + (BCD - * E /)) \rightarrow ABCD - * E / +$$
C 程式實作:中序轉後序
基本上在函式 infixToPostfix() 中,主要以五條規則下撰寫:
- 遇到運算元(數字 or 變數):直接輸出。
- 遇到左括號
(:放入堆疊。 - 遇到右括號
):將堆疊中的運算子依序 pop 並輸出,直到 pop 左括號(為止(括號不輸出)。 - 遇到運算子(
+,-,*,/):- 檢查堆疊頂端的運算子。
- 若堆疊頂端運算子優先權 $\ge$ 目前運算子:將堆疊頂端 pop 並輸出,重複此步驟直到堆疊頂端優先權較小或是遇到
(。 - 將目前的運算子放入堆疊。
- 讀取完畢:將堆疊中剩餘的所有運算子依序 pop 並輸出。
1 |
|
Output:
1 | 中序表示式: A+B*(C-D)/E |
用堆疊計算後序表示式的結果
基本上只要做兩件事情:
- 遇到運算元(Operand):直接堆入堆疊(push)。
- 遇到運算子(Operator):從堆疊拿出兩個數字運算,算完把結果放回去。
當遇到運算子(例如 /)時,要從堆疊 Pop 兩次:
- 第一次 Pop 出來的數字:是「右邊」的運算元(Right Operand)。
- 第二次 Pop 出來的數字:是「左邊」的運算元(Left Operand)。
公式:$$Value = SecondPop \ (Op) \ FirstPop$$
Op = Operator
假設有個後序表示式是 $1 \ 2 \ 5 \ 3 \ - \ * \ 2 \ / \ +$ (其對應的中序表示式是 $1 + 2 * (5 - 3) / 2$),如何用堆疊模擬後序表示式的計算結果?
| 讀取字元(next token) | 堆疊內容(stack) | 說明 |
|---|---|---|
| 1 | 1 | 數字 $\rightarrow$ push |
| 2 | 1, 2 | 數字 $\rightarrow$ push |
| 5 | 1, 2, 5 | 數字 $\rightarrow$ push |
| 3 | 1, 2, 5, 3 | 數字 $\rightarrow$ push |
- | 1, 2, 2 | 1. pop 出 3(右運算元) 2. Pop 出 5(左運算元) 3. 計算 $5 - 3 = 2$ 4. 將 2 push 回去 |
* | 1, 4 | 1. pop 出 2 2. pop 出 2 3. 計算 $2 * 2 = 4$ 4. 將 4 push 回去 |
| 2 | 1, 4, 2 | 數字 $\rightarrow$ push |
/ | 1, 2 | 1. pop 出 2(除數) 2. pop 出 4(被除數) 3. 計算 $4 / 2 = 2$ 4. 將 2 push 回去 |
+ | 3 | 1. pop 出 2 2. pop 出 1 3. 計算 $1 + 2 = 3$ 4. 將 2 push 回去 |
| none | 3 | 讀取完畢,堆疊內唯一的數字即為答案。 |
C 程式實作:計算後序表示式
註:以下程式碼僅展示後序表示式的數值計算,並未將前面的中序轉後序程式做結合。另外該版本程式碼支援多位數的計算。
1 |
|
總整理
堆疊(Stack)
特性:
- 後進先出(LIFO)
- 僅能在頂端(Top)進行操作(Push / Pop)
- Push、Pop 皆為 O(1)
應用:
- Ctrl + Z(復原)
- 函式呼叫(Call Stack)
- 括號匹配
- DFS(深度優先搜尋)
優點:
- 操作速度快、實作簡單
- 記憶體連續、效率高(系統層面尤為重要)
缺點:
- 容易 Stack Overflow(遞迴過深)
- 不支援隨機存取
Stack 是只看最頂端的資料結構,適合處理「巢狀結構」與「反向回復」問題。
佇列(Queue)
特性:
- 先進先出(FIFO)
- 使用 Front / Rear 兩端操作
- Enqueue / Dequeue 皆為 O(1)(指標移動)
應用:
- CPU 排程
- IO Buffer
- BFS(廣度優先搜尋)
- 生產者—消費者模型(如 Kafka、RabbitMQ)
優點:
- 公平、有序,適合排隊處理任務
- 可作為系統模組間的緩衝區
缺點:
- 一般陣列實作會有「假溢位」問題
- 搜尋效率低($O(n)$)
環狀佇列(Circular Queue)
主要用於解決一般 Queue 的「假溢位」問題。
核心技巧:
- 使用模運算
%,讓索引循環 - 邏輯上首尾相接,實體仍為陣列
兩種實作策略:
- 空位版:
- 判斷滿:(rear + 1) % MAX == front
- 判斷空:front == rear
- 缺點:浪費一格空間
- 無空位版:
- 使用 count 紀錄目前元素數
- 判斷空:count == 0
- 判斷滿:count == MAX
- 空間利用率 100%
環狀佇列只索引循環,不搬資料。
堆疊應用:後序表示式(Postfix / RPN)
為何需要後序表示式?
中序表示式(如 1 + 2)對電腦不友善,需處理優先順序與括號。
後序表示式:
- 不需要括號
- 非常適合用 Stack 處理
- 時間複雜度 $O(N)$
中序轉後序(Infix → Postfix)
堆疊法核心規則(五條):
- 運算元 → 直接輸出
- 左括號
(→ push - 右括號
)→ pop 直到遇到( - 運算子 →
- Stack 頂端優先權 ≥ 自己 → pop
- 否則 push
- 讀完後 → pop 剩餘運算子
重要觀念:左括號在 Stack 內的優先權最低。
括號法核心規則:
- 前處理:從優先級最高的運算子開始補括號,已經有括號的就不用補了。
- 拆括號:從最內層開始拆解,
(x op y)會轉成x y op。
計算後序表示式
演算法:
- 運算元 → push
- 運算子 → pop 兩次計算,再 push 回去
重要順序:
- 第一次 pop → 右運算元
- 第二次 pop → 左運算元
公式:$$Value = SecondPop \ (Op) \ FirstPop$$
最後 Stack 中唯一剩下的數字就是答案。

