---
# System prepended metadata

title: 【考試向】資料結構筆記（堆疊、佇列、環狀佇列、後序表示式）
tags: [資料結構, 程式設計, 電腦]

---

# 【考試向】資料結構筆記（堆疊、佇列、環狀佇列、後序表示式）

[TOC]

歡迎你點入本篇文章！我是 LukeTseng，本系列文章主要整理自學資料結構的一些知識，如果你喜歡我的文章，麻煩您不吝嗇的在文章底下按下一顆愛心，或是追蹤我唷～

## 堆疊（Stack）

特性：後進先出（LIFO：**L**ast **I**n **F**irst **O**ut）

資料只能在頂端（Top）做堆入（Push），跟移除（Pop）。

![Stack](https://hackmd.io/_uploads/rJDhAiLN-g.png)

Image Source：[Stack Data Structure and Implementation in Python, Java and C/C++](https://www.programiz.com/dsa/stack)

應用：

- 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 是否為空。

```c=
#include <stdio.h>
#include <stdbool.h>

#define MAX 5

int stack[MAX];

int top = -1;

bool empty(){
    return top == -1;
}

void push(int data){
    if (top == MAX - 1){
        printf("堆疊已滿:資料 %d 無法堆入堆疊\n", data);
    }
    else{
        top++;
        stack[top] = data;
        printf("資料 %d 堆入堆疊\n", data);
    }
}

int pop(){
    if (empty()){
        printf("堆疊為空:無法 pop\n");
        return -1;
    }
    else{
        int data = stack[top];
        top--;
        return data;
    }
}

void printStack() {
    printf("目前堆疊內容: ");
    if (empty()) {
        printf("[空]\n");
        return;
    }
    for (int i = 0; i <= top; i++) printf("%d ", stack[i]);
    printf(" <- Top\n");
}

int main() {
    push(10);
    push(20);
    push(30);
    printStack();

    int val = pop();
    printf("取出的值為:%d\n", val);
    printStack();

    push(40);
    push(50);
    push(60);
    push(70);
    printStack();

    while (!empty()) {
        pop();
    }
    pop();

    return 0;
}
```

## 佇列（Queue）

特性：先進先出（FIFO：**F**irst **I**n **F**irst **O**ut）

想像陣列分為兩端開口：前端（Front）跟尾端（Rear）。

可以做到加入（enqueue）、移除（dequeue）元素這兩種操作。

![Queue](https://hackmd.io/_uploads/rJmuH2U4We.png)

Image Source：[What is Queue Data Structure? - GeeksforGeeks](https://www.geeksforgeeks.org/dsa/what-is-queue-data-structure/)

應用：

- 作業系統層面：
    - CPU 工作排程：利用佇列管理待執行的程序（Processes），確保每個任務按順序或優先級獲得處理時間。
    - ![Task Manager](https://hackmd.io/_uploads/HkzbU3UVbg.png)
    - IO 緩衝區（Buffering）：當資料產生速度快於處理速度時（如印表機排隊列印、磁碟寫入等等），佇列可暫存資料避免遺失情形發生。
- BFS（Breadth First Search：廣度優先搜尋）

時間複雜度：

- Enqueue：$O(1)$
- Dequeue：$O(1)$（不移動元素，只移動指標）

優點：

- 保證公平性與順序：處理多執行緒任務排程或網路封包時很重要。
- 解耦（Decoupling）：在分散式系統中，佇列（如 RabbitMQ, Kafka）可作為生產者與消費者之間的緩衝，讓雙方不需要同時在線，增加系統穩定性。

缺點：

- 假溢位問題：若用簡單陣列實作，當前端資料 dequeue 後，後面的資料若不往前移，就會浪費前端的記憶體空間。而一旦往前移的話，就會額外耗費 $O(N)$ 時間。
- 搜尋效率低：若要確認某個特定資料是否在佇列中，必須遍歷整個佇列 ($O(n)$)。

### 實作方式

使用 C 語言實作。

輸出結果顯示一般佇列實作方式的問題，移除資料時，剩餘資料沒有往前移，因此導致後面要加入資料進去就加不進去，這情況差不多就像是下圖那樣：

![Queue disadvantage](https://hackmd.io/_uploads/Hk_-NqvEbl.png)

Image Source：作者繪製。

程式實作講解：

- 雙指標法：變數 `int front = -1`、`int rear = -1` 各別追蹤前端、尾端。
    - enqueue 時 `rear` 增加。
    - dequeue 時 `front` 增加。
- `rear == MAX - 1` 判別一個佇列是否已滿。
- `front == -1 && rear == -1` 判別一個佇列是否為空。
- 在 `dequeue` 時做 `front >= rear` 判別，把兩個變數初始化成 `-1` ，免得 `front` 越加越多。

```c=
#include <stdio.h>
#include <stdbool.h>

#define MAX 5

int queue[MAX];
int front = -1;
int rear = -1;

bool empty(){
    return front == -1 && rear == -1;
}

void enqueue(int data){
    if (rear == MAX - 1){
        printf("佇列已滿:資料 %d 無法加入\n", data);
    }
    else{
        if (front == -1){
            front = 0;
        }
        rear++;
        queue[rear] = data;
        printf("加入資料 %d\n", data);
    }
}

int dequeue(){
    if (empty()){
        printf("佇列為空:無法取出資料");
        return -1;
    }
    else{
        int data = queue[front];
        if (front >= rear){
            front = -1;
            rear = -1;
            printf("取出資料 %d [佇列已空]\n", data);
        }
        else{
            front++;
            printf("取出資料 %d\n", data);
        }
        return data;
    }
}

void printQueue() {
    printf("目前佇列: ");
    if (empty()) {
        printf("[空]\n");
        return;
    }
    printf("Front -> ");
    for (int i = front; i <= rear; i++) {
        printf("%d ", queue[i]);
    }
    printf("<- Rear\n");
}

int main() {
    enqueue(10);
    enqueue(20);
    enqueue(30);
    printQueue();

    dequeue();
    printQueue();
    
    enqueue(40);
    enqueue(50);
    printQueue();

    enqueue(60);

    return 0;
}
```

Output：

```
加入資料 10
加入資料 20
加入資料 30
目前佇列: Front -> 10 20 30 <- Rear
取出資料 10
目前佇列: Front -> 20 30 <- Rear
加入資料 40
加入資料 50
目前佇列: Front -> 20 30 40 50 <- Rear
佇列已滿:資料 60 無法加入
```

## 環狀佇列（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` 判斷佇列是否滿了。因為這裡，才會把空位留在那不能加入。

```c=
#include <stdio.h>
#include <stdbool.h>

#define MAX 5

int queue[MAX];
int front = 0;
int rear = 0;

bool empty(){
    return front == rear;
}

void enqueue(int data){
    if ((rear + 1) % MAX == front){
        printf("佇列已滿:資料 %d 無法加入\n", data);
    }
    else{
        queue[rear] = data;
        rear = (rear + 1) % MAX;
        printf("加入資料 %d\n", data);
    }
}

int dequeue(){
    if (empty()){
        printf("佇列為空:無法取出資料");
        return -1;
    }
    else{
        int data = queue[front];
        front = (front + 1) % MAX;
        printf("取出資料 %d\n", data);
        return data;
    }
}

void printQueue() {
    printf("目前佇列: ");
    if (empty()) {
        printf("[空]\n");
        return;
    }
    printf("Front -> ");
    int i = front;
    while (i != rear) {
        printf("%d ", queue[i]);
        i = (i + 1) % MAX; // 移動到下一格
    }
    
    printf("<- Rear\n");
}

int main() {
    enqueue(10);
    enqueue(20);
    enqueue(30);
    enqueue(40);
    printQueue();

    enqueue(50); 
    
    dequeue();
    dequeue();
    printQueue();

    enqueue(50);
    enqueue(60);
    printQueue();

    return 0;
}
```

Output：

```
加入資料 10
加入資料 20
加入資料 30
加入資料 40
目前佇列: Front -> 10 20 30 40 <- Rear
佇列已滿:資料 50 無法加入
取出資料 10
取出資料 20
目前佇列: Front -> 30 40 <- Rear
加入資料 50
加入資料 60
目前佇列: Front -> 30 40 50 60 <- Rear
```

### 實作方式 B：無空位版

改進方式是在原有的程式碼中，新增 `int count = 0;`，去判斷目前有多少個元素，剩下邏輯不變。

基本上就是判斷目前佇列大小。

- `count == 0;` 判斷佇列是否為空。
- `count == MAX;` 判斷佇列是否滿了。

```c=
#include <stdio.h>
#include <stdbool.h>

#define MAX 5

int queue[MAX];
int front = 0;
int rear = 0;
int count = 0;

bool empty(){
    return count == 0;
}

void enqueue(int data){
    if (count == MAX){
        printf("佇列已滿:資料 %d 無法加入\n", data);
    }
    else{
        queue[rear] = data;
        rear = (rear + 1) % MAX;
        count++;
        printf("加入資料 %d\n", data);
    }
}

int dequeue(){
    if (empty()){
        printf("佇列為空:無法取出資料");
        return -1;
    }
    else{
        int data = queue[front];
        front = (front + 1) % MAX;
        count--;
        printf("取出資料 %d\n", data);
        return data;
    }
}

void printQueue() {
    printf("目前佇列: ");
    if (empty()) {
        printf("[空]\n");
        return;
    }
    printf("Front -> ");
    int temp = front;
    for (int i = 0; i < count; i++) {
        printf("%d ", queue[temp]);
        temp = (temp + 1) % MAX; // 移動臨時指標
    }
    
    printf("<- Rear\n");
}

int main() {
    enqueue(10);
    enqueue(20);
    enqueue(30);
    enqueue(40);
    printQueue();

    enqueue(50);
    printQueue();
    
    dequeue();
    dequeue();
    printQueue();

    enqueue(50);
    enqueue(60);
    printQueue();

    return 0;
}
```

Output：

```
加入資料 10
加入資料 20
加入資料 30
加入資料 40
目前佇列: Front -> 10 20 30 40 <- Rear
加入資料 50
目前佇列: Front -> 10 20 30 40 50 <- Rear
取出資料 10
取出資料 20
目前佇列: Front -> 30 40 50 <- Rear
加入資料 50
加入資料 60
目前佇列: Front -> 30 40 50 50 60 <- Rear
```

## 堆疊應用：後序表示式（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$$ 

優點：

1. 不需要括號
2. 電腦好處理：電腦只要從左讀到右，遇到數字就存起來，遇到運算子就抓最近的兩個數字運算，非常適合以堆疊（Stack）來實作。

時間複雜度： $O(N)$ ，但是因為用堆疊實作，一些操作基本上是常數時間，執行速度上來說會比中序還快。

### 中序轉後序（堆疊法）

用堆疊將中序轉後序，主要依照以下規則：

1. 遇到運算元（數字 or 變數）：直接輸出。
2. 遇到左括號 `(`：放入堆疊。
3. 遇到右括號 `)`：將堆疊中的運算子依序 pop 並輸出，直到 pop 左括號 `(` 為止（括號不輸出）。
4. 遇到運算子（`+`, `-`, `*`, `/`）：
    - 檢查堆疊頂端的運算子。
    - 若堆疊頂端運算子優先權 $\ge$ 目前運算子：將堆疊頂端 pop 並輸出，重複此步驟直到堆疊頂端優先權較小或是遇到 `(`。
    - 將目前的運算子放入堆疊。
5. 讀取完畢：將堆疊中剩餘的所有運算子依序 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. 比較 `*` 與 `/`。<br>2. `*` 優先順序 $\ge$ `/`，pop `*`。<br>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()` 中，主要以五條規則下撰寫：

1. 遇到運算元（數字 or 變數）：直接輸出。
2. 遇到左括號 `(`：放入堆疊。
3. 遇到右括號 `)`：將堆疊中的運算子依序 pop 並輸出，直到 pop 左括號 `(` 為止（括號不輸出）。
4. 遇到運算子（`+`, `-`, `*`, `/`）：
    - 檢查堆疊頂端的運算子。
    - 若堆疊頂端運算子優先權 $\ge$ 目前運算子：將堆疊頂端 pop 並輸出，重複此步驟直到堆疊頂端優先權較小或是遇到 `(`。
    - 將目前的運算子放入堆疊。
5. 讀取完畢：將堆疊中剩餘的所有運算子依序 pop 並輸出。

```c=
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <ctype.h>
#include <string.h>

#define MAX 100

char stack[MAX];
int top = -1;

bool isEmpty(){
    return top == -1;
}

void push(char i){
    if (top >= MAX - 1){
        printf("Stack Overflow\n");
        return;
    }
    stack[++top] = i;
}

char pop(){
    if (isEmpty()){
        printf("Stack Underflow\n");
        return -1;
    }
    return stack[top--];
}

char peek(){
    if (isEmpty()) return -1;
    return stack[top];
}

int getPriority(char op){
  switch (op){
    case '*':
    case '/':
    case '%':
      return 2;
    case '+':
    case '-':
      return 1;
    case '(':
      return 0;
    default:
      return -1;
  }
}

void infixToPostfix(char* infix, char* postfix){
  int i = 0; // infix 的索引
  int j = 0; // postfix 的索引
  char token;
  
  while ((token = infix[i++]) != '\0'){
    
    // 1. 如果是運算元 (字母或數字)，直接輸出
    if (isalnum(token)){
      postfix[j++] = token;
      postfix[j++] = ' '; // 美觀用
    }
    // 2. 如果是左括號，直接 push 進堆疊
    else if (token == '('){
      push(token);
    }
    // 3. 如果是右括號，pop 直到遇到左括號
    else if (token == ')'){
      while (!isEmpty() && peek() != '('){
        postfix[j++] = pop();
        postfix[j++] = ' ';
      }
      if (!isEmpty() && peek() == '('){
        pop(); // 把左括號丟掉，不輸出
      }
    }
    // 4. 如果是運算子
    else if (token == '+' || token == '-' || token == '*' || token == '/' || token == '%'){
      // 當堆疊不為空，且堆疊頂端優先權 >= 目前運算子優先權
      // 就要把堆疊頂端踢出來 (Pop)
      while (!isEmpty() && getPriority(peek()) >= getPriority(token)){
        postfix[j++] = pop();
        postfix[j++] = ' ';
      }
      // 踢完之後，把自己放進去
      push(token);
    }
  }
  // 5. 處理剩餘的運算子
  while (!isEmpty()){
    postfix[j++] = pop();
    postfix[j++] = ' ';
  }
  
  postfix[j] = '\0'; // 記得加上字串結束符號
}

int main(){
  char infix[] = "A+B*(C-D)/E";
  char postfix[MAX];
  
  printf("中序表示式: %s\n", infix);
  
  infixToPostfix(infix, postfix);
  
  printf("後序表示式: %s\n", postfix);
  
  return 0;
}
```

Output：

```
中序表示式: A+B*(C-D)/E
後序表示式: A B C D - * E / + 
```

### 用堆疊計算後序表示式的結果

基本上只要做兩件事情：

1. 遇到運算元（Operand）：直接堆入堆疊（push）。
2. 遇到運算子（Operator）：從堆疊拿出兩個數字運算，算完把結果放回去。

當遇到運算子（例如 `/`）時，要從堆疊 Pop 兩次：

1. 第一次 Pop 出來的數字：是「右邊」的運算元（Right Operand）。
2. 第二次 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（右運算元）<br>2. Pop 出 5（左運算元）<br>3. 計算 $5 - 3 = 2$<br>4. 將 2 push 回去 |
| `*` | 1, 4 | 1. pop 出 2<br>2. pop 出 2<br>3. 計算 $2 * 2 = 4$<br>4. 將 4 push 回去 |
| 2 | 1, 4, 2 | 數字 $\rightarrow$ push |
| `/` | 1, 2 | 1. pop 出 2（除數）<br>2. pop 出 4（被除數）<br>3. 計算 $4 / 2 = 2$<br>4. 將 2 push 回去 |
| `+` | 3 | 1. pop 出 2<br>2. pop 出 1<br>3. 計算 $1 + 2 = 3$<br>4. 將 2 push 回去 |
| none | 3 | 讀取完畢，堆疊內唯一的數字即為答案。 |

### C 程式實作：計算後序表示式

註：以下程式碼僅展示後序表示式的數值計算，並未將前面的中序轉後序程式做結合。另外該版本程式碼支援多位數的計算。

```cpp=
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

#define MAX 100

int stack[MAX];
int top = -1;

void push(int i){
  if (top >= MAX - 1){
    printf("Stack Overflow\n");
    return;
  }
  stack[++top] = i;
}

int pop(){
  if (top == -1){
    printf("Stack Underflow\n");
    exit(1);
  }
  return stack[top--];
}

int evalPostfix(char* postfix){
  int i = 0;
  
  while (postfix[i] != 0){
    if (postfix[i] == ' '){
      i++;
      continue;
    }
    
    if (isdigit(postfix[i])){
      int num = 0;
      
      while (isdigit(postfix[i])){
        num = num * 10 + (postfix[i] - '0');
        i++;
      }
      push(num);
    }
    else{
      int val2 = pop();
      int val1 = pop();
      
      switch(postfix[i]){
        case '+': push(val1 + val2); break;
        case '-': push(val1 - val2); break;
        case '*': push(val1 * val2); break;
        case '/': 
            push(val1 / val2); 
            break;
        case '%': 
            push(val1 % val2); 
            break;
        default: 
            printf("Unknown operator: %c\n", postfix[i]); 
            exit(1);
      }
      i++;
    }
  }
  return pop();
}

int main() {
    char postfix[] = "1 2 5 3 - * 2 / +";
    
    printf("後序表示式: %s\n", postfix);
    
    int result = evalPostfix(postfix);
    
    printf("計算結果: %d\n", result);
    
    return 0;
}
```

## 總整理

### 堆疊（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 的「假溢位」問題。

核心技巧：

- 使用模運算 `%`，讓索引循環
- 邏輯上首尾相接，實體仍為陣列

兩種實作策略：

1. 空位版：
    - 判斷滿：(rear + 1) % MAX == front
    - 判斷空：front == rear
    - 缺點：浪費一格空間
2. 無空位版：
    - 使用 count 紀錄目前元素數
    - 判斷空：count == 0
    - 判斷滿：count == MAX
    - 空間利用率 100%

環狀佇列只索引循環，不搬資料。

### 堆疊應用：後序表示式（Postfix / RPN）

為何需要後序表示式？

中序表示式（如 1 + 2）對電腦不友善，需處理優先順序與括號。

後序表示式：

- 不需要括號
- 非常適合用 Stack 處理
- 時間複雜度 $O(N)$

### 中序轉後序（Infix → Postfix）

堆疊法核心規則（五條）：

1. 運算元 → 直接輸出
2. 左括號 `(` → push
3. 右括號 `)` → pop 直到遇到 `(`
4. 運算子 →
    - Stack 頂端優先權 ≥ 自己 → pop
    - 否則 push
5. 讀完後 → pop 剩餘運算子

重要觀念：左括號在 Stack 內的優先權最低。

---

括號法核心規則：

1. 前處理：從優先級最高的運算子開始補括號，已經有括號的就不用補了。
2. 拆括號：從最內層開始拆解， `(x op y)` 會轉成 `x y op` 。

### 計算後序表示式

演算法：

1. 運算元 → push
2. 運算子 → pop 兩次計算，再 push 回去

重要順序：

1. 第一次 pop → 右運算元
2. 第二次 pop → 左運算元

公式：$$Value = SecondPop \ (Op) \ FirstPop$$

最後 Stack 中唯一剩下的數字就是答案。

