# 【考試向】資料結構筆記(堆疊、佇列、環狀佇列、後序表示式) [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 中唯一剩下的數字就是答案。