# 演算法 Algorithm **演算法是一個有限的指令集,用以完成指定的工作。** ## 定義: 1. **Input(明確輸入):** 零個或多個輸入。 2. **Output(輸出):** 至少產生一個輸出。 3. **Definiteness(明確性):** 每一步驟必須是明確的,不能含糊不清。 4. **Finiteness(有限性):** 演算法必須在有限的步驟內完成。 5. **Effectiveness(有效性):** 每一步驟都必須是基本的,可以被執行的。 - 演算法(Algorithm) + 資料結構(Data Structure) = 程式(Program) - 複雜的演算法通常被分割為細小的模組(Module),以便於理解與維護。 # 遞迴 Recursion **遞迴是一種在函式內部呼叫自己來解決問題的方法。遞迴機制非常強大,因為它可以清楚的表達複雜的問題,並且通常能夠簡化程式碼。** ## 三種遞迴 1. **直接遞迴 (Direct Recursion):** 函式直接呼叫自己。 2. **間接遞迴 (Indirect Recursion):** 函式 A 呼叫函式 B,函式 B 再呼叫函式 A。 3. **尾遞迴 (Tail Recursion):** 遞迴呼叫是函式的最後一個動作,可以被優化以節省記憶體。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image.png) ## 與非遞迴函數的比較 | 特性 | 遞迴函數(Recursive Function) | 非遞迴函數(Non-Recursive Function) | | -------- | ---------------------------- | ---------------------------------- | | 可讀性 | 通常較容易 | 可能較不易 | | 簡潔性 | 通常較緊湊、簡潔 | 可能較複雜、冗長 | | 執行效率 | 可能較低(因為函式呼叫開銷) | 通常較高 | # 空間複雜度(Space Complexity) **空間複雜度是指演算法在執行過程中所需的記憶體空間量,通常以輸入資料的大小來表示。** **空間複雜度通常被區分為下列兩部分:** 1. **固定部分 (Fixed Part):** 指令本身的空間,例如簡單的變數、常數等。 2. **變動部分 (Variable Part):** 資料所需的空間(遞迴的呼叫堆疊之類的)。 根據一個程式 P 的空間需求 S(p),可以表示為: ``` S(p) = c + Sp ``` 其中 c 為固定部分,Sp 為變動部分;而我們通常關注的是 Sp,因為它會隨著輸入資料的大小而改變。 # 時間複雜度 **時間複雜度是指演算法在執行過程中所需的時間量,通常以輸入資料的大小來表示。** 對於一個程式 P 的時間需求 T(p),可以表示為: ``` T(p) = c + Tp ``` 其中 c 是編譯時間(compile time),而 Tp 則是運行時間(run|execution time);我們通常關注的是 Tp,因為它會隨著輸入資料的大小而改變。 有兩種方式可以勘定時間複雜度: 1. **測量(Measurement):** 透過實際執行程式並測量其運行時間來估算時間複雜度(執行程式並記錄 CPU 時間)。 2. **分析(Analysis):** 計算程式步驟或是指令的數量。 ## 時間複雜度的表示法 我們希望能用一些符號表示一個帶有 n 輸入的演算法函數 f(n)的時間或空間複雜度,接下來會介紹一些符號表示不精確但有意義的陳述。 ## Big-Oh (O(n)): 定義: ``` O(g(n)) = { f(n) | 若且唯若「存在」正的常數c和n0,使得對所有n >= n0,有0 <= f(n) <= c*g(n) } ``` **對於當 n >= n0 的所有 n,在 f(n) = O(g(n)) 中,c\*g(n) 是 f(n) 的一個上界。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-1.png) ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-2.png) - 為使描述 f(n) = O(g(n)) 有意義,應選擇一個盡可能小的 g(n)。 - 常見的時間複雜度類型: |名字|符號| |---|---| |常數時間(constant)| O(1)| |對數時間(logarithmic)| O(log n)| |線性時間(linear)| O(n)| |線性對數時間(linearithmic)| O(n log n)| |平方時間(quadratic)| O(n^2)| |指數時間(exponential)| O(2^n)| |立方時間(cubic)| O(n^3)| |階乘時間(factorial)| O(n!)| |多項式時間(polynomial)| O(n^k) (k 為常數)| |指數時間(exponential)| O(k^n) (k 為常數)| |超指數時間(super-exponential)| O(n^n)| 排序如下: ``` O(1) < O(log n) < O(n) < O(n log n) < O(n^2) < O(n^3) < O(n^k) < O(2^n) < O(k^n) < O(n!) < O(n^n) ``` ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-3.png) ## Omega (Ω(n)) 定義: ``` Ω(g(n)) = { f(n) | 若且唯若「存在」正的常數c和n0,使得對所有n >= n0,有0 <= c*g(n) <= f(n) } ``` **對於當 n >= n0 的所有 n,在 f(n) = Ω(g(n)) 中,c\*g(n) 是 f(n) 的一個下界。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-4.png) ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-5.png) - 為使描述 f(n) = Ω(g(n)) 有意義,應選擇一個盡可能大的 g(n)。 ## Theta (Θ(n)) 定義: ``` Θ(g(n)) = { f(n) | 若且唯若「存在」正的常數c1、c2和n0,使得對所有n >= n0,有0 <= c1*g(n) <= f(n) <= c2*g(n) } ``` **是一個比 Big-Oh 和 Omega 更加精確的表示法,c\*g(n) 同時是 f(n) 的上界和下界。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-6.png) ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-7.png) ## 其他的表示法 1. 小 o 符號 (little o notation): 定義: ``` o(g(n)) = { f(n) | 對「所有」正的常數c,存在一個常數n0,使得對所有n >= n0,有0 <= f(n) < c*g(n) } ``` **表示 f(n) 增長速度比 g(n) 慢得多。** 跟 Big-Oh 的差別在於,Big-Oh 允許 f(n) 和 g(n) 增長速度相同,而小 o 則不允許。 2. 小 ω 符號 (little omega notation): 定義: ``` ω(g(n)) = { f(n) | 對「所有」正的常數c,存在一個常數n0,使得對所有n >= n0,有0 <= c*g(n) < f(n) } ``` **表示 f(n) 增長速度比 g(n) 快得多。** 跟 Omega 的差別在於,Omega 允許 f(n) 和 g(n) 增長速度相同,而小 ω 則不允許。 ## 一些轉換(可能不用記...嗎?) 1. 若 f(n) = Θ(g(n)) 且 g(n) = Θ(h(n)),則 f(n) = Θ(h(n))。 2. 若 f(n) = O(g(n)) 且 g(n) = O(h(n)),則 f(n) = O(h(n))。 3. 若 f(n) = Ω(g(n)) 且 g(n) = Ω(h(n)),則 f(n) = Ω(h(n))。 4. 若 f(n) = o(g(n)) 且 g(n) = o(h(n)),則 f(n) = o(h(n))。 5. 若 f(n) = ω(g(n)) 且 g(n) = ω(h(n)),則 f(n) = ω(h(n))。 6. f(n) = Θ(g(n)) 若且唯若 g(n) = Θ(f(n))。 7. f(n) = O(g(n)) 若且唯若 g(n) = Ω(f(n))。 8. f(n) = o(g(n)) 若且唯若 g(n) = ω(f(n))。 9. f(n) = Θ(f(n))。 10. f(n) = O(f(n))。 11. f(n) = Ω(f(n))。 # 大師法則 (Master Method) **大師法則是一種用來分析分治演算法時間複雜度的工具。** ## 大師法則的形式 對於一個遞迴關係式: ``` T(n) = aT(n/b) + f(n) ``` 其中: - a >= 1 是子問題的數量。 - b > 1 是每個子問題的規模縮小比例。 - f(n) 是合併子問題結果所需的時間。 ## 大師法則的三種情況 1. **情況 1:** 如果存在一個常數 ε > 0,使得: ``` f(n) = O(n^(log_b(a) - ε)) ``` 那麼: ``` T(n) = Θ(n^(log_b(a))) ``` 2. **情況 2:** 如果存在一個常數 k >= 0,使得: ``` f(n) = Θ(n^(log_b(a)) * log^k(n)) ``` 那麼: ``` T(n) = Θ(n^(log_b(a)) * log^(k+1)(n)) ``` 3. **情況 3:** 如果存在一個常數 ε > 0,使得: ``` f(n) = Ω(n^(log_b(a) + ε)) ``` 且如果存在一個常數 c < 1,使得對所有足夠大的 n,有: ``` a * f(n/b) <= c * f(n) ``` 那麼: ``` T(n) = Θ(f(n)) ``` # 陣列 Array **陣列是含有多個鍵值對(pairs <index, value>)的資料結構,並使每個索引(index)都對到一個唯一的值(value)。** ## 2D 陣列 - **2D 陣列是陣列的陣列,又被稱為矩陣。** - **一般由 m 個行(row)和 n 個列(column)組成,若行列數相等則為方陣(square matrix)。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-8.png) - **對於矩陣 A,元素 A~i,j~ = A[i-1][j-1]** - **矩陣能以兩種方法表示:** ## 1. 行主序(row-major order): - **對於矩陣 A,行主序會將 A~1,1~、A~1,2~、...、A~1,n~ 依序存放,接著是 A~2,1~、A~2,2~、...、A~2,n~,以此類推。** ``` (3x3 matrix) [27,31, -4, 5, 8, 16, => [27,31,-4,5,8,16,7,10,15] 7,10, 15] (A1,1,A1,2,A1,3,A2,1,A2,2,A2,3,A3,1,A3,2,A3,3) ``` - **對於 m x n 的矩陣 A,元素 Ai,j 在一維陣列中的位置為:** ``` ls + [(i - 1) * n + (j - 1)] * d ``` 其中 ls 為陣列的起始位置,d 為每個元素的大小。 ## 2. 列主序(column-major order): - **對於矩陣 A,列主序會將 A1,1、A2,1、...、Am,1 依序存放,接著是 A1,2、A2,2、...、Am,2,以此類推。** ``` (3x3 matrix) [27,31, -4, 5, 8, 16, => [27,5,7,31,8,10,-4,16,15] 7,10, 15] (A1,1,A2,1,A3,1,A1,2,A2,2,A3,2,A1,3,A2,3,A3,3) ``` - **對於 m x n 的矩陣 A,元素 A~i,j~ 在一維陣列中的位置為:** ``` ls + [(j - 1) * m + (i - 1)] * d ``` 其中 ls 為陣列的起始位置,d 為每個元素的大小。 ## 三角矩陣 ### 1. 下三角矩陣 (Lower Triangular Matrix) - **一個方陣(Square Matrix)A 的第 i 行最大的非零項目數是 i,且當 i < j 時,A~i,j~ = 0,這樣的矩陣稱為下三角矩陣。** - **下三角矩陣的非零項目數為:** ``` n(n + 1) / 2 ``` ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-9.png) - ## 位置表示 - ## 行主序(row-major order): **對於 n x n 的下三角矩陣 A,元素 A~i,j~ 在一維陣列中的位置為:** ``` ∀i ≥ j, Location(Ai,j) = ls + [(i * (i - 1)) / 2 + j - 1] * d ``` 其中 ls 為陣列的起始位置,d 為每個元素的大小。 - ## 列主序(column-major order): **對於 n x n 的下三角矩陣 A,元素 A~i,j~ 在一維陣列中的位置為:** ``` ∀i ≥ j, Location(Ai,j) = ls + [((2n - j)(j - 1)) / 2 + i - 1] * d ``` 其中 ls 為陣列的起始位置,d 為每個元素的大小。 ### 2. 上三角矩陣 (Upper Triangular Matrix) - **一個方陣(Square Matrix)A 的第 i 行最小的非零項目數是 i,且當 i > j 時,Ai,j = 0,這樣的矩陣稱為上三角矩陣。** - **上三角矩陣的非零項目數為:** ``` n(n + 1) / 2 ``` ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-10.png) - ## 位置表示 - ## 行主序(row-major order): **對於 n x n 的上三角矩陣 A,元素 A~i,j~ 在一維陣列中的位置為:** ``` ∀i ≤ j, Location(Ai,j) = ls + [((2n - i)(i - 1)) / 2 + j - 1] * d ``` 其中 ls 為陣列的起始位置,d 為每個元素的大小。 - ## 列主序(column-major order): **對於 n x n 的上三角矩陣 A,元素 A~i,j~ 在一維陣列中的位置為:** ``` ∀i ≤ j, Location(Ai,j) = ls + [(j * (j - 1)) / 2 + i - 1] * d ``` 其中 ls 為陣列的起始位置,d 為每個元素的大小。 # 鏈結串列 Linked List **鏈結串列是一種動態資料結構,由一系列節點(node)組成,每個節點包含一個或多個資料和指向下一個節點的指標(pointer)。** ## 單向鏈結串列 (Singly Linked List) - **每個節點包含兩個部分: 資料(data)和指向下一個節點的指標(next pointer)。** - **最後一個節點的 next 指標指向 null,表示鏈結串列的結束。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-11.png) ## 環狀鏈結串列 (Circular Linked List) - **最後一個節點的 next 指標指向鏈結串列的第一個節點,形成一個環狀結構。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-12.png) ## 雙向鏈結串列 (Doubly Linked List) - **每個節點包含三個部分: 資料(data)、指向下一個節點的指標(next pointer)和指向前一個節點的指標(prev pointer)。** - **這種結構允許雙向遍歷鏈結串列。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-13.png) ## 雙向環狀鏈結串列 (Doubly Circular Linked List) - **最後一個節點的 next 指標指向鏈結串列的第一個節點,而第一個節點的 prev 指標指向最後一個節點,形成一個雙向環狀結構。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-14.png) ## 其與陣列之比較 | 特性 | 陣列(Array) | 鏈結串列(Linked List) | | -------------- | ---------------------- | ------------------------ | | 記憶體配置 | 靜態配置,大小固定 | 動態配置,大小可變 | | 連續性 | 元素連續存放 | 元素不一定連續存放 | | 存取方式 | 透過索引存取 | 需從頭節點遍歷 | | 存取時間 | O(1) (透過索引) | O(n) (需遍歷) | | 插入/刪除時間 | O(n) (需移動元素) | O(1) (只需更新指標) | | 記憶體使用效率 | 可能浪費空間(預留空間) | 節點需額外指標空間 | | 資料局部性 | 較好,適合快取 | 較差,可能導致快取未命中 | # 堆疊 Stack **堆疊是一種後進先出(Last In First Out, LIFO)的資料結構,允許在一端進行插入(push)和刪除(pop)操作。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-19.png) ## 以鏈結串列實作堆疊 - **若資料大小無法確定,則能以鏈結串列實作堆疊。** - **堆疊的頂端(top)指向鏈結串列的第一個節點。** - ## 操作 1. **Push(插入):** - **建立一個新節點,將資料存入,並將其 next 指標指向目前的頂端節點,然後更新頂端指標指向新節點。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-15.png) 2. **Pop(刪除):** - **檢查堆疊是否為空,若不為空,則將頂端指標指向下一個節點,並釋放原頂端節點的記憶體。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-16.png) 3. **Peek(查看頂端元素):** - **返回頂端節點的資料,但不刪除該節點。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-17.png) ## 堆疊排列 Stack Permutation - **給定一個輸入序列,堆疊排列是指透過堆疊操作所能產生的所有可能輸出序列。** - **例如,對於輸入序列 [1, 2, 3],可能的堆疊排列包括:** ``` [1, 2, 3] => push 1, pop 1, push 2, pop 2, push 3, pop 3 [1, 3, 2] => push 1, pop 1, push 2, push 3, pop 3, pop 2 [2, 1, 3] => push 1, push 2, pop 2, pop 1, push 3, pop 3 [2, 3, 1] => push 1, push 2, pop 2, push 3, pop 3, pop 1 [3, 2, 1] => push 1, push 2, push 3, pop 3, pop 2, pop 1 ``` - **並非所有排列都能透過堆疊操作產生,例如 [3, 1, 2] 無法透過堆疊操作達成。** - ## 卡塔蘭數 (Catalan Number) - **卡塔蘭數是一系列自然數,常用於計算堆疊排列的數量。第 n 個卡塔蘭數 C(n) 定義為:** ``` C(n) = (1 / (n + 1)) * (2n choose n) = (2n)! / ((n + 1)! * n!) ``` ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-18.png) - **對於 n 個元素的輸入序列,可能的堆疊排列數量為第 n 個卡塔蘭數 C(n)。** # 佇列 Queue **佇列是一種先進先出(First In First Out, FIFO)的資料結構,允許在一端(rear)進行插入操作,在另一端(front)進行刪除操作。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-20.png) ## 以陣列實作佇列 - **使用固定大小的陣列來存放佇列元素,並使用兩個指標(front 和 rear)來追蹤佇列的前端和後端位置。** - **當 rear 到達陣列末端時,可以將其重新指向陣列的起始位置,形成環狀佇列(circular queue)。** - ## 操作 0. **宣告** ```c++ #define MAX 10 //改變此值將會改變陣列大小 int queue[MAX]; int front = -1, rear = -1; void insert(void); int delete_element(void); void display(void); ``` 1. **Insert(插入):** - **將新元素加入到 rear 指標所指的位置,然後更新 rear 指標指向下一個位置。** ```c++ void insert() { int num; printf("Enter a number to insert: "); scanf("%d", &num); if(rear == MAX-1) printf("\n OVERFLOW"); else if(front == -1 && rear == -1)front = rear = 0; else { rear++; queue[rear] = num; } } ``` 2. **Delete(刪除):** - **從 front 指標所指的位置移除元素,然後更新 front 指標指向下一個位置。** ```c++ int delete_element() { if(front == -1){ printf("\n UNDERFLOW"); return -1; } else { val = queue[front]; front++; if(front > rear) front = rear = -1; return val; } } ``` 3. **Display(查看前端元素):** - **返回 front 指標所指的元素,但不刪除該元素。** ```c++ void display() { int i; printf("\n"); if(front == -1) printf("Queue is empty"); else { for(i = front; i <= rear; i++) { printf("\t %d", queue[i]); } } } ``` ## 以鏈結串列實作佇列 - **比起使用陣列,鏈結串列在插入和刪除操作上更具彈性,無需考慮容量限制。** - ## 操作 0. **Declare(宣告)** ```c++ #include <stdio.h> #include <conio.h> #include <malloc.h> struct node { int data; struct node *next; }; struct queue { struct node *front; struct node *rear; }; struct queue *q; void create_queue(struct queue *); struct queue *insert(struct queue *, int); struct queue *delete_element(struct queue *); ``` 1. **Create(建立):** - **初始化佇列,將 front 和 rear 指標設為 null。** ```c++ void create_queue(struct queue *q) { q->front = NULL; q->rear = NULL; } ``` 2. **Insert(插入):** - **建立一個新節點,將資料存入,並將其加入到佇列的 rear 位置,然後更新 rear 指標。** ```c++ struct queue *insert(struct queue *q, int value) { struct node *new_node; new_node = (struct node *)malloc(sizeof(struct node)); new_node->data = value; if(q->front == NULL) { q->front = new_node; q->rear = new_node; q->front->next = NULL; q->rear->next = NULL; } else { q->rear->next = new_node; q->rear = new_node; q->rear->next = NULL; } return q; } ``` 3. **Delete(刪除):** - **從佇列的 front 位置移除元素,然後更新 front 指標。** ```c++ struct queue *delete_element(struct queue *q) { struct node *temp; temp = q->front; if(q->front == NULL) { printf("\n UNDERFLOW"); } else { q->front = q->front->next; printf("\n Deleted element is %d", temp->data); free(temp); } return q; } ``` ## 各種變體 1. ## 環形佇列 (Circular Queue) - **環形佇列是一種特殊的佇列,當 rear 指標到達陣列末端時,會重新指向陣列的起始位置,形成一個環狀結構。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-25.png) - **無法對已滿的佇列進行插入,即便刪除了元素,其已經不具備可被插入的性質。** 2. ## 雙端佇列 (Deque - Double-Ended Queue) - **雙端佇列允許在兩端進行插入和刪除操作,提供更大的靈活性。** - **具有兩個指標,RIGHT 以及 LEFT,任何插入刪除行為無法在中間進行。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-24.png) - ## 輸入限制雙端佇列 (Input-Restricted Deque) **只能在一端進行插入操作,但可以在兩端進行刪除操作。** - ## 輸出限制雙端佇列 (Output-Restricted Deque) **可以在兩端進行插入操作,但只能在一端進行刪除操作。** 3. ## 優先佇列 (Priority Queue) - **優先佇列是一種特殊的佇列,其中每個元素都有一個優先級,元素被處理的順序取決於其優先級,而非插入順序;具有相同優先級的元素會按照插入順序處理(FCFS, First-Come-First-Served)。** - **廣泛適用於各種操作系統,如煞車優先系統(BOS, Brake Override System)。** - ## 實作方法(Array) - **每個優先級(priority)都有自己的佇列,以及對應的 front 和 rear 指標。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-21.png) - **插入操作根據元素的優先級將其加入對應的佇列。(以插入元素 R 的優先級為 3 的範例)** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-22.png) - ## 實作方法(鏈結串列) - **每個節點都有三個部分:資料區、優先級區和指向下一個節點的指標。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-23.png) - **此鏈結串列具以下特點,舉例來說:** 1. **由於 A 具有比 B 更高的優先級(1<2),因此 A 會被插入到 B 的前面。** 2. **C 和 D 具有相同的優先級(2),因此它們依先來後到被排列並被處理。** 3. **優先級以適當將元素排列,我們無法得知低優先級的元素是否比高優先級還要先到。** # 算術表示法 Arithmetic Expression **算術表示法是用來表示數學運算的符號系統,常見的表示法有中序表示法(Infix Notation)、前序表示法(Prefix Notation)和後序表示法(Postfix Notation)。** - **為什麼我們習慣使用中序表示法,卻還需要前序和後序表示法呢?** 對於人類來說,中序表示法較為直觀且易於理解,因為它符合我們日常使用的數學表達方式。然而,對於計算機來說,中序表示法需要額外的規則來處理運算優先級和括號,這使得解析和計算變得複雜。前序和後序表示法則不需要括號,且運算優先級是由符號的位置決定的,因此更適合計算機進行處理。 - **舉例來說:** | 表示法 | 例子 | | ------ | ---------- | | 中序 | A + B \* C | | 前序 | + A \* B C | | 後序 | A B C \* + | - **簡單將中序表示轉換為前/後序表示的描述:** 1. 將表達式完全補上括號 2. 將所有運算子取代其對應的左/右括號 3. 移除所有括號 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-26.png) ## 將中序轉後序的演算法 1. **建立一個空的堆疊(stack)和一個空的輸出串列(output list)。** 2. **從左到右掃描中序表示法的每個符號:** - **如果符號是運算元(operand),將其加入輸出串列。** - **如果符號是左括號'(',將其推入堆疊。** - **如果符號是右括號')',則從堆疊中彈出運算子並加入輸出串列,直到遇到左括號為止,並取出左括號。** - **如果符號是運算子(operator),則從堆疊中彈出運算子並加入輸出串列,直到堆疊頂端的運算子優先級低於當前運算子,然後將當前運算子推入堆疊。** **運算子優先級如下:** |優先級|運算子| |---|---| |1| -(一元負號)| |2| not| |3| \*. /, %, &| |4| +, -, \^(xor), \| | |5| <, <=, >, >=, =, != | 3. **掃描結束後,將堆疊中剩餘的運算子全部彈出並加入輸出串列。** 4. **輸出串列即為後序表示法。** - **舉 (A - (B / C +(D % E \* F) / G) \* H) 為例:** | 步驟 | 符號 | 堆疊(Stack) | 輸出串列(Output List) | | ---- | ---- | ------------------ | -------------------------- | | 1 | \( | \( | | | 2 | A | \( | A | | 3 | - | \(- | A | | 4 | \( | \(-\( | A | | 5 | B | \(-\( | AB | | 6 | / | \(-\(/ | AB | | 7 | C | \(-\(/ | ABC | | 9 | + | \(-\(+ | ABC/ | | 10 | \( | \(-\(+\( | ABC/ | | 11 | D | \(-\(+\( | ABC/D | | 12 | % | \(-\(+\(% | ABC/D | | 13 | E | \(-\(+\(% | ABC/DE | | 14 | \* | \(-\(+\(\* | ABC/DE% | | 15 | F | \(-\(+\(\* | ABC/DE%F | | 16 | \) | \(-\(+ | ABC/DE%F\* | | 17 | / | \(-\(+/ | ABC/DE%F\* | | 18 | G | \(-\(+/ | ABC/DE%F\*G | | 19 | \) | \(- | ABC/DE%F\*G/+ | | 20 | \* | \(-\* | ABC/DE%F\*G/+ | | 21 | H | \(-\* | ABC/DE%F\*G/+H | | 22 | \) | | ABC/DE%F\*G/+H\*- | ## 計算後序表示法的值 1. **建立一個空的堆疊(stack)。** 2. **從左到右掃描後序表示法的每個符號:** - **如果符號是運算元(operand),將其推入堆疊。** - **如果符號是運算子(operator),則從堆疊中彈出兩個運算元,根據運算子進行計算 B op A(A 是最上面彈出的運算元,B 是 A 下面的的運算元),然後將結果推入堆疊。** 3. **掃描結束後,堆疊中唯一的元素即為後序表示法的計算結果。** - **舉 934\*8+4/- 為例:** | 步驟 | 符號 | 堆疊(Stack) | | ---- | ---- | ------------------ | | 1 | 9 | 9 | | 2 | 3 | 9, 3 | | 3 | 4 | 9, 3, 4 | | 4 | \* | 9, 12 | | 5 | 8 | 9, 12, 8 | | 6 | + | 9, 20 | | 7 | 4 | 9, 20, 4 | | 8 | / | 9, 5 | | 9 | - | 4 | # 樹 Tree **樹是一種非線性(non-linear)資料結構,由節點(node)組成,節點之間透過邊(edge)連接,形成層次(hierarchical)結構。** - **對於下圖的樹:** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-27.png) - 節點 B, C, D 是節點 A 的子節點(children)。 - 節點 A 是節點 B, C, D 的父節點(parent)。 - **根節點(root node):** 代表整個樹的頂端節點,在此例中為節點 A。 - **葉/終端節點(leaf/ terminal node):** 沒有子節點的節點,在此例中為節點 E, F, J, K, H, I。 - **子樹(subtree):** 由某節點及其所有後代節點所組成的樹,如 T1, T2, T3。 - **路徑(path):** 由一連串節點所組成的序列,從一個節點到另一個節點的連接。如: 從節點 A 到節點 I 的路徑為 `A -> D -> I`。 - **祖輩節點(ancestor node):** 某節點的所有上層節點,從該節點一直到根節點的路徑上的節點。如: 節點 I 的祖先節點為 D 和 A。 - **孫輩節點(descendant node):** 某節點的所有下層節點,從該節點一直到葉節點的路徑上的節點。如: 節點 E 和 F 是節點 B 的孫輩節點。 - **層數(level number):** 節點所在的層次,根節點的層數為 0,其子節點的層數為 1,依此類推。如:節點 H 和 I 的層數為 2。 - **度(degree):** 節點的子節點數量。如: 節點 A 的度為 3,節點 B 的度為 2,節點 E 的度為 0。 ## 二元樹 Binary Tree **二元樹是一種特殊的樹結構,其中每個節點最多有 0, 1, 2 個子節點,分別稱為左子節點(left child)和右子節點(right child)。** - **手足節點(sibling node):** 具有相同階層及父節點的節點稱為手足節點。 - **高度(height):** 節點到其最遠葉節點的節點總數;一棵高度為 h 的二元樹最少有 2^h - 1 個節點。 - **形似(similar):** 兩棵二元樹若其結構相同,則稱為形似的二元樹。 - **複製(copies):** 將一棵二元樹形似且資料一致於另一棵二元樹,稱為複製。 - ## 將通用樹(General Tree)轉換為二元樹(Binary Tree) **將通用樹的每個節點的第一個子節點作為二元樹的左子節點,其他子節點則依序作為右子節點。** **例如,通用樹中的節點 A 有三個子節點 B, C, D,在轉換後的二元樹中,B 成為 A 的左子節點,C 成為 B 的右子節點,D 成為 C 的右子節點。** - ## 實作(Array) - **使用陣列來表示二元樹,對於節點在陣列中的位置 i,其左子節點的位置為 2i + 1,右子節點的位置為 2i + 2,父節點的位置為 (i - 1) / 2。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-28.png) - ## 實作(Linked List) - **使用節點結構來表示二元樹,每個節點包含資料(data)、指向左子節點的指標(left pointer)和指向右子節點的指標(right pointer)。** ```c++ struct node { int data; struct node *left; struct node *right; }; ``` ## 完全二元樹 Complete Binary Tree **完全二元樹是一種特殊的二元樹,除了最後一層外,每一層的節點數量都達到最大值,且最後一層的節點從左至右依序填滿。** - **例如,以下的二元樹為完全二元樹:** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-29.png) - **而以下的二元樹則不是完全二元樹,因為最後一層的節點沒有從左至右依序填滿:** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-30.png) ## 延展二元樹 Extended Binary Tree - **延展二元樹是一種特殊的二元樹,其中每個非葉節點都有兩個子節點,葉節點則沒有子節點。** - **為了達成這個目的,延展二元樹會在原本的葉節點位置插入額外的空節點(null node),使得每個非葉節點都有兩個子節點。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-31.png) ## 競賽/選擇樹 Tournament/Selection Tree - **競賽樹是一種特殊的二元樹,用於模擬比賽過程,其中每個非葉節點代表一場比賽的勝者,葉節點則代表參賽者。** - **在競賽樹中,葉節點通常存放參賽者的資料,而非葉節點則存放比賽結果,如勝者的資料或分數。** ## 二元樹的遍歷 Binary Tree Traversal **二元樹的遍歷是指按照特定順序訪問二元樹中的每個節點。常見的遍歷方法有前序遍歷(preorder traversal)、中序遍歷(inorder traversal)和後序遍歷(postorder traversal)。** - **前序遍歷(Preorder Traversal):** 先訪問根節點,然後遞迴訪問左子樹,最後遞迴訪問右子樹。 - **中序遍歷(Inorder Traversal):** 先遞迴訪問左子樹,然後訪問根節點,最後遞迴訪問右子樹。 - **後序遍歷(Postorder Traversal):** 先遞迴訪問左子樹,然後遞迴訪問右子樹,最後訪問根節點。 - **階序遍歷(Level-order Traversal):** 按照層次從上到下、從左到右依序訪問節點。 - **例如,對於以下的二元樹:** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-32.png) - 前序遍歷結果為: A, B, D, C, E, F, G, H, I - 中序遍歷結果為: B, D, A, E, H, G ,I, F, C - 後序遍歷結果為: D, B, H, I, G, F, E, C, A - 階序遍歷結果為: A, B, C, D, E, F, G, H, I - ### 藉由遍歷構建二元樹 **給定前序和中序遍歷序列,可以唯一確定一棵二元樹,並透過遞迴方法構建該二元樹。** **同理,給定後序和中序遍歷序列也可以唯一確定一棵二元樹,並透過遞迴方法構建該二元樹。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-34.png) - #### 補充:可定義二元樹的方法(Midterm 2023) 透過任選集合 A 和集合 B,各選一項即可定義二元樹: - 集合 A: 1. 階序 Level-order 2. 前序 Pre-order 3. 後序 Post-order - 集合 B: 1. 二元搜尋樹 Binary Search Tree 2. 完全二元樹 Complete Binary Tree 3. 中序 In-order ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-33.png) ## 引線二元樹 Threaded Binary Tree **引線二元樹是一種特殊的二元樹,利用空指標來建立節點之間的連結,使得在不使用堆疊或遞迴的情況下也能進行遍歷。** **在引線二元樹中,若節點的左子指標為 null,則將其指向該節點在中序遍歷中的前驅(predecessor)節點;若節點的右子指標為 null,則將其指向該節點在中序遍歷中的後繼(successor)節點。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-35.png) - ## 單路徑引線二元樹(One-way Threaded/Single-Threaded Binary Tree) **每個節點只有一個空指標被用來建立引線,若右側出現引線,則他會指向該節點在中序遍歷中的後繼(successor)節點,此樹稱右引線二元樹(right-threaded binary tree);而若左側出現引線,則他會指向該節點在中序遍歷中的前驅(predecessor)節點,此樹稱左引線二元樹(left-threaded binary tree)。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-36.png) - ## 雙路徑引線二元樹(Two-way Threaded/Double-Threaded Binary Tree)/完全引線二元樹(Full Threaded Binary Tree) **每個節點的兩個空指標都被用來建立引線,左側的引線指向該節點在中序遍歷中的前驅(predecessor)節點,右側的引線指向該節點在中序遍歷中的後繼(successor)節點。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-37.png) # 各種重要的二元樹變體 ## 二元搜尋樹 Binary Search Tree **二元搜尋樹是一種特殊的二元樹,其中每個節點的左子樹包含的所有節點值均小於該節點值,右子樹包含的所有節點值均大於該節點值。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-38.png) ### 搜尋 Search - **從根節點開始,若搜尋值小於當前節點值,則遞迴搜尋左子樹;若搜尋值大於當前節點值,則遞迴搜尋右子樹;若相等則找到該節點。** - **c++ 範例:** ```c++ node *search(node *root, int key) { if(root == NULL) return &nullNode;// A nullNode is a global node indicating not found if (root->data == key) return root; if (key < root->data) return search(root->left, key); return search(root->right, key); } ``` ### 插入 Insert - **從根節點開始,若插入值小於當前節點值,則遞迴插入左子樹;若插入值大於當前節點值,則遞迴插入右子樹;直到找到適當的空位置,將新節點插入。** - **c++ 範例:** ```c++ node* insert(node* root, int key) { if (root == NULL) { return newNode(key); } if (key < root->data) { root->left = insert(root->left, key); } else { root->right = insert(root->right, key); } return root; } ``` ### 刪除 Delete - **從根節點開始,尋找要刪除的節點。若找到該節點,根據其子節點數量進行刪除操作:** 1. **若該節點沒有子節點,直接刪除該節點。** 2. **若該節點有一個子節點,將該節點的父節點指向其唯一的子節點,然後刪除該節點。** 3. **若該節點有兩個子節點,找到該節點右子樹中的最小值節點(或左子樹中的最大值節點),將其值複製到要刪除的節點,然後遞迴刪除該最小值(或最大值)節點。** - **c++ 範例:** ```c++ node* deleteNode(node* root, int key) { if (root == NULL) return root; if (key < root->data) { root->left = deleteNode(root->left, key); } else if (key > root->data) { root->right = deleteNode(root->right, key); } else { // Node with only one child or no child if (root->left == NULL) { node* temp = root->right; free(root); return temp; } else if (root->right == NULL) { node* temp = root->left; free(root); return temp; } // Node with two children: Get the inorder successor (smallest in the right subtree) node* temp = minValueNode(root->right); root->data = temp->data; root->right = deleteNode(root->right, temp->data); } return root; } ``` ## AVL 樹 AVL Tree **AVL 樹是一種自平衡的二元搜尋樹,透過在每個節點維護一個平衡因子(balance factor),並使每個節點的平衡因子為-1、0 或 1 ,以此確保樹的高度保持在 O(log n) 的範圍內。** **一個合法的 AVL 樹有三種情況:** 1. **根節點平衡因子為 0,表示左子樹和右子樹高度相等。(Balanced)** 2. **根節點平衡因子為 1,表示左子樹高度比右子樹高 1。(Left heavy)** 3. **根節點平衡因子為 -1,表示右子樹高度比左子樹高 1。(Right heavy)** ### 搜尋 Search - **與二元搜尋樹的搜尋方法相同,從根節點開始,根據搜尋值與當前節點值的比較結果,遞迴搜尋左子樹或右子樹,直到找到該節點或達到葉節點。** ### 插入 Insert - **與二元搜尋樹的插入方法相同,將新節點插入到適當的位置。** - **插入新節點後,從插入點回溯至根節點,更新每個節點的高度和計算平衡因子。若發現某個節點的平衡因子不在 -1 至 1 範圍內,則進行適當的旋轉操作以恢復平衡。** - **旋轉操作包括:** 1. **雙右臂旋(Right-Right Rotation):** 用於左子樹過高的情況。 - 插入 89 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-40.png) 插入 89 後,使得根節點的平衡因子為-2,是 Right heavy 的情況,因此需要進行雙右臂旋來恢復平衡。 取根節點的右節點 63 作為新的根節點,並將原本的根節點 45 設為 63 的左子節點,63 的左子節點 54 設為 45 的右子節點。 2. **雙左臂旋(Left-Left Rotation):** 用於右子樹過高的情況。 - 插入 18 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-39.png) 插入 18 後,使得根節點的平衡因子為 2,是 Left heavy 的情況,因此需要進行雙左臂旋來恢復平衡。 取根節點 36 作為新的根節點,並將原本的根節點 45 設為 36 的右子節點,36 的右子節點 39 設為 45 的左子節點。 3. **左右臂旋(Left-Right Rotation):** 用於左子樹的右子樹過高的情況。 - 插入 37 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-41.png) 插入 37 後,使得根節點的平衡因子為 2,是 Left heavy 的情況,但根節點 45 的左子節點 36 的平衡因子為-1,是 Right heavy 的情況。 因此需要先對 36 進行右臂旋,再對根節點 45 進行左臂旋來恢復平衡。 4. **右左臂旋(Right-Left Rotation):** 用於右子樹的左子樹過高的情況。 - 插入 60 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-42.png) 插入 60 後,使得根節點的平衡因子為-2,是 Right heavy 的情況,但根節點 45 的右子節點 63 的平衡因子為 1,是 Left heavy 的情況。 因此需要先對 63 進行左臂旋,再對根節點 45 進行右臂旋來恢復平衡. ### 刪除 Delete - **與二元搜尋樹的刪除方法相同,尋找並刪除指定節點。** - **刪除節點後,從刪除點回溯至根節點,更新每個節點的高度和計算平衡因子。若發現某個節點的平衡因子不在 -1 至 1 範圍內,則進行適當的旋轉操作以恢復平衡。** - **旋轉操作包括:** 1. **R0 旋轉(R0 Rotation):** 用於旋轉後的新根原平衡因子為 0 的情況,包含 RR 或 LL 旋轉。 - 刪除 72 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-43.png) 刪除 72 後,使得根節點的平衡因子為 2,是 Left heavy 的情況,因此需要進行 R0 旋轉來恢復平衡。 取根節點 45 的左子節點 36 作為新的根節點,並將原本的根節點 45 設為 36 的右子節點,36 的右子節點 39 設為 45 的左子節點。 2. **R1 旋轉(R1 Rotation):** 用於旋轉後的新根原平衡因子為 1 的情況,包含 LL 或 RL 旋轉。 - 刪除 72 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-45.png) 刪除 72 後,使得根節點的平衡因子為 2,是 Left heavy 的情況,取根節點 45 的左子節點 36 作為新的根節點,而其平衡因子為 1,因此需要進行 R1 旋轉來恢復平衡。 又因為 36 的平衡因子為 1,是 Left heavy 的情況,所以在兩個節點都為 Left heavy 的情況下,直接對根節點 45 進行雙左臂旋來恢復平衡。 3. **R-1 旋轉(R-1 Rotation):** 用於旋轉後的新根原平衡因子為 -1 的情況,包含 RR 或 LR 旋轉。 - 刪除 72 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-44.png) 刪除 72 後,使得根節點的平衡因子為 2,是 Left heavy 的情況,取根節點 45 的左子節點 36 作為新的根節點,而其平衡因子為-1,因此需要進行 R-1 旋轉來恢復平衡。 又因為 36 的平衡因子為-1,是 Right heavy 的情況,因此需要先對 36 進行右臂旋,再對根節點 45 進行左臂旋來恢復平衡。 ## 2-3 樹 2-3 Tree **2-3 樹是一種自平衡的多路搜尋樹,其中每個節點可以有兩個或三個子節點,並且包含一至兩筆資料。(是一種 Degree = 3 的 B 樹)** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-50.png) ### 搜尋 Search - **與二元搜尋樹的搜尋方法相同,從根節點開始,根據搜尋值與當前節點資料的比較結果,遞迴搜尋適當的子節點,直到找到該資料或達到葉節點。** ### 插入 Insert - **從根節點開始,尋找適當的葉節點位置插入新資料。** - **若葉節點有空位,直接插入新資料。** - **若葉節點已滿(包含兩筆資料),則將該節點分裂為兩個節點,並將中間的資料提升至父節點。** - **若父節點也已滿,則遞迴進行分裂操作,直到達到根節點。** - **若根節點分裂,則創建一個新的根節點,使樹的高度增加一層。** - 插入 37 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-46.png) 插入 37 後,葉節點 [37, 39, 45] 已滿,因此將其分裂為兩個節點 [37] 和 [45],並將中間的資料 39 提升至父節點。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-47.png) ### 刪除 Delete - **從根節點開始,尋找要刪除的資料所在的節點。** - **當刪除的是內部節點時,則需在移除資料後,尋找其後中序表示法的繼節點進行替換。** - **當刪除的是葉節點時,直接刪除該資料。** - **若上述任何行為導致節點資料數量低於最低要求(即 1 筆資料),則需進行合併或借用操作以恢復平衡。** 1. **當一個點為空點時,需要借用其與手足節點(sibling node)夾擠的父節點的資料,並形成一個新的節點。** 2. **當上述行為使得節點數超過最大值(即 2 筆資料),則需進行分裂操作,將中間的資料提升至父節點。** 3. **若有任一節點不合法,則重複上述步驟直到整棵樹合法為止。** - **一個例子** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-48.png) ## 紅黑樹 Red-Black Tree **紅黑樹是一種自平衡的二元搜尋樹,其中每個節點都被標記為紅色或黑色,並且遵循以下規則以確保樹的平衡:** 1. **每個節點要麼是紅色,要麼是黑色。** 2. **根節點必須是黑色。** 3. **所有葉節點(空節點)都是黑色。** 4. **如果一個節點是紅色,則其子節點必須是黑色(即不允許連續的紅色節點)。** 5. **從任一節點到其所有後代葉節點的路徑上,必須包含相同數量的黑色節點。** 6. **從根節點出發的最長路徑不會超過最短路徑的兩倍長度。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-49.png) ### 搜尋 Search - **與二元搜尋樹的搜尋方法相同,從根節點開始,根據搜尋值與當前節點值的比較結果,遞迴搜尋左子樹或右子樹,直到找到該節點或達到葉節點。** ### 插入 Insert - **與二元搜尋樹的插入方法相同,將新節點插入到適當的位置,並將其標記為紅色。** - **插入新節點後,檢查並修正紅黑樹的規則,可能需要進行重新著色(recoloring)和旋轉(rotation)操作以恢復平衡。** - **重新著色和旋轉操作包括:** - **重新著色(Recoloring):** 若出現有一個黑色節點擁有兩個紅色子節點的情況,則將該黑色節點重新著色為紅色,並將其兩個紅色子節點重新著色為黑色。 - **旋轉(Rotation):** 若出現連續的紅色節點,則根據情況進行左臂旋(left rotation)或右臂旋(right rotation)來恢復平衡。 - **插入一個元素的執行規則:** 1. **搜尋適合的位置用以插入新節點。** 2. **若在搜尋過程中,出現了有兩個紅色子節點的黑色節點:** 1. **將該黑色節點重新著色為紅色,並將其兩個紅色子節點重新著色為黑色。** 2. **尋找是否出現連續的紅色節點,若有則進行旋轉操作以恢復平衡。** 3. **插入欲插入的元素並將其標記為紅色。** 4. **尋找是否出現連續的紅色節點,若有則進行旋轉操作以恢復平衡。** 5. **確保根節點為黑色。** **助記規則:** ` Color Change → Rotation → Insert → Rotation → Check Root` ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-51.png) ### 刪除 Delete **課上未講解刪除部分,故此部分留空。** ### 與 AVL 樹的比較 - **AVL 樹較為嚴格地保持平衡,適合讀取頻繁且寫入較少的應用場景; 紅黑樹不具如此平衡的特性,但其能保證在讀取、寫入、刪除都至少為對數時間複雜度(O(log n))。** - **兩者在相同數量級的時間複雜度下,紅黑樹通常具有較好的插入和刪除性能,因此較為靈活,其被廣泛運用在如 C++ STL 的 map 和 set 容器中。** ## B 樹 B-Tree **B 樹是一種自平衡的多路搜尋樹(multi-way search tree),對於階數(或稱為度, degree)為 m 的 B 樹,可以有 m-1 筆資料和 m 個指向子節點的指標。** **其作為多路搜尋樹的特殊版本,除了具備上述特性,B 樹具有以下額外的特性:** 1. **根節點至少有兩個子節點。** 2. **每個節點最多有 m 個子節點(即最多有 m-1 筆資料)。** 3. **每個非根節點至少有⌈m/2⌉個子節點(即至少有⌈m/2⌉-1 筆資料)。** 4. **所有葉節點都位於同一階層。** - m = 4(最多 4 個、最少 2 個子節點,最多 3 筆、最少 1 筆資料)的 B 樹範例: ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-52.png) ### 搜尋 Search - **與二元搜尋樹的搜尋方法相似,從根節點開始,根據搜尋值與當前節點資料的比較結果,遞迴搜尋適當的子節點,直到找到該資料或達到葉節點。** ### 插入 Insert - **從根節點開始,尋找適當的葉節點位置插入新資料。** - **若葉節點有空位,直接插入新資料並排序。** - **若葉節點已滿(包含 m-1 筆資料),則將該節點分裂為兩個節點,並將中間的資料提升至父節點。(如同 2-3 樹的分裂操作)** - **一個較為複雜,需要多次分裂的插入範例:(m = 5, 最多 5 個、最少 3 個子節點,最多 4 筆、最少 2 筆資料)** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-53.png) ### 刪除 Delete - **依照刪除位置及各節點元素數量的情況有所不同:** | 刪除位置 | 狀態 | 處理方式 | | ---- | ------------- | ------------------ | | 內部節點 | — | 用前驅或後繼替代,再轉為葉節點刪除 | | 葉節點 | 鍵值 > 最小 | 直接刪除 | | 葉節點 | 鍵值 = 最小,左兄弟可借 | 左兄弟最大鍵上推 → 父鍵下拉 | | 葉節點 | 鍵值 = 最小,右兄弟可借 | 右兄弟最小鍵上推 → 父鍵下拉 | | 葉節點 | 左右兄弟都不可借 | 合併節點 → 若父節點不足則向上調整 | - **一個較為複雜,需要多次合併的刪除範例:(m = 5, 最多 5 個、最少 3 個子節點,最多 4 筆、最少 2 筆資料)** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-54.png) ## B+ 樹 B+ Tree **B+ 樹是一種自平衡的多路搜尋樹,是 B 樹的變體,其主要特點包括:** - **所有資料都存放在葉節點(硬碟),內部節點僅用於索引(記憶體)。** - **葉節點之間透過鏈結串列(linked list)相連,便於範圍查詢(range query)和順序存取(sequential access)。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-55.png) ### 搜尋 Search - **與二元搜尋樹的搜尋方法相同,從根節點開始,根據搜尋值與當前節點資料的比較結果,遞迴搜尋適當的子節點,直到找到該資料或達到葉節點。** ### 插入 Insert - **從根節點開始,尋找適當的葉節點位置插入新資料。** - **若葉節點有空位,直接插入新資料並排序。** - **若葉節點已滿(包含 m-1 筆資料),則將該節點分裂為兩個節點,並將右節點的最小值作為索引複製到父節點** - **若是對內部節點(索引)進行分裂,則不須複製,直接提升。** - **一個較為複雜,需要多次分裂的插入範例:(m = 4, 最多 4 個、最少 2 個子節點,最多 3 筆、最少 1 筆資料)** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-56.png) ### 刪除 Delete - **對於 B+ 樹的刪除操作,主要涉及葉節點的資料刪除,並根據需要進行節點的合併或借用,以維持樹的平衡和結構完整性。** - **刪除葉節點相關操作後可能會遇到兩種情況:** 1. **若葉節點的資料數量低於最低要求(即⌈m/2⌉-1 筆資料),則與兄弟節點進行合併,並刪除其夾擠的索引。** 2. **若內部節點(索引)的資料數量低於最低要求,則與兄弟節點以及他們夾擠的父節點合併組成新的節點。** - **相關操作舉例:(m = 4, 最多 4 個、最少 2 個子節點,最多 3 筆、最少 1 筆資料)** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-57.png) ## 二元堆積 Binary Heap **二元堆積是一種完全二元樹,分為最大堆積(max-heap)和最小堆積(min-heap)兩種形式。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-58.png) - ### 最大堆積 Max-Heap **在最大堆積中,每個節點的值都大於或等於其子節點的值,根節點擁有整個堆積中的最大值。** - ### 最小堆積 Min-Heap **在最小堆積中,每個節點的值都小於或等於其子節點的值,根節點擁有整個堆積中的最小值。** ### 插入 Insert - **將新元素插入到堆積的最後一個位置,然後依照他是 max 還是 min heap,透過「上浮(up-heap)」操作將其移動到適當的位置,以維持堆積的性質。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-59.png) ### 刪除 Delete - **二元堆積通常只允許刪除根節點(最大或最小值),刪除後,將最後一個元素移動到根節點位置,然後依照他是 max 還是 min heap,透過「下沉(down-heap)」操作將其移動到適當的位置,以維持堆積的性質。** - 逐漸讓 11 下沉 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-60.png) ## 伸展樹 Splay Tree **伸展樹是一種自調整的二元搜尋樹,透過在每次訪問節點後進行旋轉操作,將該節點移動到根節點位置,以提高後續訪問的效率。** - **伸展樹的主要旋轉操作有三項:** 1. **Zig 操作(類似 R 或 L rotation):** 當訪問的節點是其父節點的左/右子節點時,進行右/左旋轉。 2. **Zig-Zig 操作(類似 LL 或 RR rotation):** 當訪問的節點和其父節點以及祖輩都是同一邊的節點時,先對父節點進行旋轉,再對訪問的節點進行旋轉。 3. **Zig-Zag 操作(類似 LR 或 RL rotation):** 當訪問的節點是其父節點以及祖輩反邊的子節點,先對訪問的節點進行旋轉,再對訪問節點的父節點進行旋轉。 **透過這些旋轉操作,伸展樹能夠將頻繁訪問的節點移動到樹的頂端,從而提高訪問效率。** ### 搜尋 Search - **與二元搜尋樹的搜尋方法相同,從根節點開始,根據搜尋值與當前節點值的比較結果,遞迴搜尋左子樹或右子樹,直到找到該節點或達到葉節點。** - **找到該節點後,進行伸展操作,將該節點移動到根節點位置。** - **若未找到該節點,則對最後訪問的節點進行伸展操作。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-61.png) ### 插入 Insert - **與二元搜尋樹的插入方法相同,將新節點插入到適當的位置。** - **插入新節點後,對該節點進行伸展操作,將其移動到根節點位置。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-62.png) ### 刪除 Delete - **從根節點開始,尋找要刪除的節點。** - **若找到該節點,對其進行伸展操作,將其移動到根節點位置。** - **刪除根節點後,取左子樹最大或是右子樹最小的節點伸展至作為新的根節點,並合併成一棵新的樹。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-63.png) ### 優劣勢分析 Pros & Cons - **優勢**:其能夠自我調整,將頻繁訪問的節點移動到樹的頂端,提高訪問效率,很適合用在如快取(cache)等應用場景。且相較於其他自平衡樹(如 AVL 樹、紅黑樹),伸展樹的實作較為簡單,無需維護額外的平衡資訊。 - **劣勢**:由於每次訪問都會進行伸展操作,可能導致樹的結構變得不平衡,從而影響其他節點的訪問效率。 ## 霍夫曼樹 Huffman Tree **霍夫曼樹是一種用於進行霍夫曼編碼的數,其核心思想在於將最常用的資料以較短的編碼表示,而較少用的資料以較長的編碼表示,從而達到資料壓縮的目的。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-64.png) ### 加權外部路徑長度 Weighted External Path Length (WPL) **每個節點的權重(weight)通常是根據該節點所存在的階層作加權。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-66.png) - **對於 T1 來說,其 WPL = :** `2×3+3×3+5×2+11×3+5×3+2×2=77` ### 建立霍夫曼樹的步驟 Steps to Build Huffman Tree **給定一些帶有權重的節點,透過以下步驟來建立霍夫曼樹:** 1. **將所有節點按照權重從小到大排序,並將其視為獨立的樹。** 2. **從排序後的節點中,選擇兩個權重最小的節點,將它們合併成一個新的節點,該新節點的權重為兩個子節點權重之和。** 3. **將新節點插入到節點集合中,並重新排序。** 4. **重複步驟 2 和 3,直到所有節點都被合併成一棵樹為止。** - 以此類推,最終形成的樹即為霍夫曼樹。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-67.png) ### 霍夫曼編碼&解碼 Huffman Coding & Decoding **透過霍夫曼樹,可以為每個節點分配一個唯一的二進位編碼,該編碼是根據從根節點到該節點的路徑來決定的:向左走記為 0,向右走記為 1。** - **編碼 Coding:** - **從根節點開始,根據要編碼的資料,沿著霍夫曼樹的路徑移動,直到到達對應的葉節點,並將沿途的二進位編碼組合起來,即為該資料的霍夫曼編碼。** - **解碼 Decoding:** - **從霍夫曼編碼的起始位置開始,根據二進位編碼,沿著霍夫曼樹的路徑移動,遇到 0 則向左走,遇到 1 則向右走,直到到達葉節點,即為解碼後的資料。** ## 各種變體的比較 | 資料結構 | 平衡性 | 節點結構 | 主要用途 | 優勢 | 劣勢 | 插入時間複雜度 | 刪除時間複雜度 | 搜尋時間複雜度 | | --------------------------- | -------------------------------- | ---------------------------- | -------------------------- | ------------------------------- | ------------------------------ | ------------------------ | ------------------------ | -------------------------------- | | **二元搜尋樹 (BST)** | 不一定平衡 | 每個節點最多 2 個子節點 | 基本搜尋與排序 | 實作簡單、直觀 | 若輸入資料偏序,會退化成鏈 | 平均 O(log n),最差 O(n) | 平均 O(log n),最差 O(n) | 平均 O(log n),最差 O(n) | | **AVL 樹** | 嚴格平衡 (高度差 ≤ 1) | 每節點多存高度與平衡因子 | 需要快速搜尋的場景 | 搜尋效率佳 (接近理想 log n) | 插入刪除較複雜,需要旋轉 | O(log n) | O(log n) | O(log n) | | **2-3 樹** | 完全平衡 | 節點可含 2~3 子樹 | 多路搜尋基礎樹 | 高度穩定,搜尋效率穩定 | 實作複雜 | O(log n) | O(log n) | O(log n) | | **紅黑樹** | 弱平衡(黑高平衡) | 每節點多存顏色 | STL map、set、Java TreeMap | 插入刪除效率高於 AVL(少旋轉) | 搜尋稍慢於 AVL | O(log n) | O(log n) | O(log n) | | **B 樹** | 完全平衡,多路搜尋 | 每節點存多個 key 與子樹指標 | 檔案系統、資料庫索引 | 減少磁碟存取次數 | 實作較複雜 | O(log n) | O(log n) | O(log n) | | **B+ 樹** | 完全平衡,所有資料在葉節點 | 內部節點只存索引,葉節點串接 | 資料庫、檔案系統最常用索引 | 區間搜尋快速、磁碟存取效率高 | 搜尋單一值時需要到葉節點 | O(log n) | O(log n) | O(log n) | | **二元堆積 (Binary Heap)** | 不保證 BST 性質,只保證堆序 | 完全二元樹 | 優先佇列 (Priority Queue) | 取最大/最小值極快 | 無法快速搜尋特定值 | O(log n) | O(log n) | **O(1)** 取極值,搜尋其他值 O(n) | | **伸展樹 (Splay Tree)** | 根據存取頻率自我調整,不嚴格平衡 | 普通 BST 節點 | 熱點資料常被重複查詢 | 近期存取的元素極快 (平攤效率好) | 單次操作最差 O(n) | 均攤 O(log n) | 均攤 O(log n) | 均攤 O(log n) | | **霍夫曼樹 (Huffman Tree)** | 無需平衡,依頻率建滿二元樹 | 每節點存編碼權重 | 資料壓縮 (如 ZIP, JPEG) | 能產生最省空間的前綴碼 | 只能用於編碼,不能用於一般查詢 | 建樹 O(n log n) | 無刪除操作概念 | 編碼/解碼視樹深度,一般 O(n) | # 排序 Sorting Algorithms **排序算法是將一組資料按照特定順序重新排列的過程。** ## 冒泡排序 Bubble Sort - **概念:** 重複地遍歷要排序的資料列,依次比較相鄰的元素,若順序錯誤則交換位置,直到整個資料列有序為止。 - **時間複雜度:** 最佳 O(n^2),最差 O(n^2) - **虛擬碼 Pseudocode:** ``` for i from 0 to n-1 do for j from 0 to n-i-2 do if A[j] > A[j+1] then swap A[j] and A[j+1] ``` - **範例:** 排序: `A[] = {5, 2, 9, 1, 5, 6}` 1. 比較 A[0] 和 A[1],因為 5 > 2,交換位置: {2, 5, 9, 1, 5, 6} 2. 比較 A[1] 和 A[2],因為 5 < 9,不交換位置: {2, 5, 9, 1, 5, 6} 3. 比較 A[2] 和 A[3],因為 9 > 1,交換位置: {2, 5, 1, 9, 5, 6} 4. 重複以上步驟直到徹底掃描整個陣列 最終結果: `A[] = {1, 2, 5, 5, 6, 9}` ## 插入排序 Insertion Sort - **概念:** 將資料分為已排序和未排序兩部分,從未排序部分取出一個元素,將其插入到已排序部分的適當位置,重複此過程直到所有元素均排序完成。 - **時間複雜度:** 最佳 O(n),最差 O(n^2) - **虛擬碼 Pseudocode:** ``` for i from 1 to n-1 do key = A[i] j = i - 1 while j >= 0 and A[j] > key do A[j + 1] = A[j] j = j - 1 A[j + 1] = key ``` - **範例:** 排序: `A[] = {5, 2, 9, 1, 5, 6}` 1. 將 A[1] (2) 插入到已排序部分 {5},結果: {2, 5} 2. 將 A[2] (9) 插入到已排序部分 {2, 5},結果: {2, 5, 9} 3. 將 A[3] (1) 插入到已排序部分 {2, 5, 9},結果: {1, 2, 5, 9} 4. 重複以上步驟直到徹底掃描整個陣列 最終結果: `A[] = {1, 2, 5, 5, 6, 9}` ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-68.png) ## 二元樹排序 Tree Sort - **概念:** 將資料插入到二元搜尋樹中,然後進行中序遍歷以獲得排序後的資料。 - **時間複雜度:** 最佳 O(n log n),最差 O(n^2) ## 堆積排序 Heap Sort - **概念:** 將資料構建成最大或最小堆積,然後反覆取出堆頂元素並重建堆積,直到所有元素均排序完成。 - **時間複雜度:** 最佳 O(n log n),最差 O(n log n) ## 選擇排序 Selection Sort - **概念:** 重複地從未排序部分選擇最小(或最大)元素,將其放置在已排序部分的末尾,直到所有元素均排序完成。 - **時間複雜度:** 最佳 O(n^2),最差 O(n^2) - **虛擬碼 Pseudocode:** ``` for i from 0 to n-1 do minIndex = i for j from i+1 to n-1 do if A[j] < A[minIndex] then minIndex = j swap A[i] and A[minIndex] ``` - **範例:** 排序: `A[] = {5, 2, 9, 1, 7, 6}` 1. 找到最小元素 1,交換位置: {1, 2, 9, 5, 7, 6} 2. 找到最小元素 2,位置不變: {1, 2, 9, 5, 7, 6} 3. 找到最小元素 5,交換位置: {1, 2, 5, 9, 7, 6} 4. 重複以上步驟直到徹底掃描整個陣列 最終結果: `A[] = {1, 2, 5, 6, 7, 9}` ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-69.png) ## 合併排序 Merge Sort - **概念:** 將資料遞迴地分割成兩半,直到每個子陣列只有一個元素,然後將子陣列合併回來,並在合併過程中進行排序。 - **時間複雜度:** 最佳 O(n log n),最差 O(n log n) - **虛擬碼 Pseudocode:** ``` function mergeSort(A, left, right) if left < right then mid = (left + right) / 2 mergeSort(A, left, mid) mergeSort(A, mid + 1, right) merge(A, left, mid, right) function merge(A, left, mid, right) n1 = mid - left + 1 n2 = right - mid create arrays L[0..n1-1] and R[0..n2-1] for i from 0 to n1-1 do L[i] = A[left + i] for j from 0 to n2-1 do R[j] = A[mid + 1 + j] i = 0; j = 0; k = left while i < n1 and j < n2 do if L[i] <= R[j] then A[k] = L[i] i = i + 1 else A[k] = R[j] j = j + 1 k = k + 1 while i < n1 do A[k] = L[i] i = i + 1 k = k + 1 while j < n2 do A[k] = R[j] j = j + 1 k = k + 1 ``` - **範例:** 排序: `A[] = {5, 2, 9, 1, 7, 6}` (單次 merge 過程,目前有兩陣列 L[] = {2,5,9} 及 R[] = {1,6,7} ) - 首先合併兩陣列為 A[] = {2,5,9,1,6,7},並建立目標陣列 T[] = {}。 - 設定指標: I = 0, J = 3 使 A[I] = 2, A[J] = 1。 - 開始比較 A[I] 及 A[J]: 1. 因為 A[I] > A[J],將 A[J] 放入 T[],並使 J 右移,A[J] = 6,T[] = {1}。 2. 因為 A[I] < A[J],將 A[I] 放入 T[],並使 I 右移,A[I] = 5,T[] = {1,2}。 3. 因為 A[I] < A[J],將 A[I] 放入 T[],並使 I 右移,A[I] = 9,T[] = {1,2,5}。 4. 因為 A[I] > A[J],將 A[J] 放入 T[],並使 J 右移,A[J] = 7,T[] = {1,2,5,6}。 5. 因為 A[I] > A[J],將 A[J] 放入 T[],並使 J 右移,J 超出範圍,T[] = {1,2,5,6,7}。 6. 將剩餘的 A[I] 放入 T[],T[] = {1,2,5,6,7,9}。 最終結果: `A[] = {1, 2, 5, 6, 7, 9}` (若此次 merge 不是最後一次,則根據上述虛擬碼繼續執行合併) ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-70.png) ## 快速排序 Quick Sort - **概念:** 選擇一個基準元素(pivot),將資料分割成兩部分,一部分小於基準元素,另一部分大於基準元素,然後遞迴地對這兩部分進行排序。 - **時間複雜度:** 最佳 O(n log n),平均 O(n log n),最差 O(n^2) - **虛擬碼 Pseudocode:** ``` function quickSort(A, low, high) if low < high then pivotIndex = partition(A, low, high) quickSort(A, low, pivotIndex - 1) quickSort(A, pivotIndex + 1, high) function partition(A, low, high) pivot = A[high] i = low - 1 for j from low to high - 1 do if A[j] <= pivot then i = i + 1 swap A[i] and A[j] swap A[i + 1] and A[high] return i + 1 ``` - **範例:** 排序: `A[] = {5, 2, 9, 1, 7, 6}` (單次 partition 過程) 1. 選擇基準元素 5 進行 partition,使 left 及 pivot 在其上,right 在 6 上。 2. 因為 right > pivot,right 左移至 7(仍然合法)。 3. 直至 right 移至 1(不合法),停止。 4. 交換 left 及 right 的值,並使 pivot 仍然於 5 的位置,結果: {1, 2, 9, 5, 7, 6} 5. left 在 1 上,尚未達到停止條件,繼續右移至 2(合法),再右移至 9(不合法),停止。 6. 交換 left 及 right 的值,並使 pivot 仍然於 5 的位置,結果: {1, 2, 5, 9, 7, 6} 7. right 在 9 上,尚未達到停止條件,繼續左移至 5,此時 left 與 right 相遇,停止。 最終結果: `A[] = {1, 2, 5, 6, 7, 9}` ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-98.png) ## 基數排序 Radix Sort - **概念:** 根據每個元素的位數,從最低位到最高位依次進行排序,通常使用穩定排序算法(如計數排序)作為子排序算法。 - **時間複雜度:** O(kn),其中 k 為最大數字的位數。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-71.png) ## 希爾排序 Shell Sort - **概念:** 將資料的長度除以 2 為 gap,把每單位 gap 的資料切開疊起來,縱向即為子陣列,對每個子陣列做插入排序,重複此過程直到 gap 為 1。 - **時間複雜度:** 最佳 O(n),最差 O(n^2) - **虛擬碼 Pseudocode:** ``` function shellSort(A) gap = length(A) while gap > 1 do gap = (gap + 1) / 2 for i from 0 to gap - 1 do insertionSortWithGap(A, i, gap) ``` - **範例:** 排序: `A[] = {5, 2, 9, 1, 7, 6}` 1. 初始 gap = 6,更新 gap = 3,分成三個子陣列: {5, 1}, {2, 7}, {9, 6}。 2. 對每個子陣列進行插入排序,結果: {1, 5}, {2, 7}, {6, 9},合併後: {1, 2, 6, 5, 7, 9}。 3. 更新 gap = 2,分成兩個子陣列: {1, 6, 7}, {2, 5, 9}。 4. 對每個子陣列進行插入排序,結果: {1, 5, 6, 7}, {2, 9},合併後: {1, 2, 5, 6, 7, 9}。 5. 更新 gap = 1,對整個陣列進行插入排序,結果: {1, 2, 5, 6, 7, 9}。 最終結果: `A[] = {1, 2, 5, 6, 7, 9}` ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-99.png) ## 性質-穩定性 Stability - **穩定排序 Stable Sort:** 若兩個元素相等,排序後其相對位置不變。例如: 冒泡排序、插入排序、合併排序、二元樹排序、基數排序。 - **不穩定排序 Unstable Sort:** 若兩個元素相等,排序後其相對位置可能改變。例如: 選擇排序、快速排序、堆積排序、歇爾排序。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-72.png) ## 各種排序算法比較 Comparison of Sorting Algorithms | 演算法名稱 | 英文名稱 | 最佳時間複雜度 (Best) | 最差時間複雜度 (Worst) | 穩定性 (Stability) | | ---------- | ---------------- | --------------------- | ---------------------- | ------------------ | | 冒泡排序 | Bubble Sort | O(n^2) | O(n^2) | 穩定 (Stable) | | 插入排序 | Insertion Sort | O(n) | O(n^2) | 穩定 (Stable) | | 選擇排序 | Selection Sort | O(n^2) | O(n^2) | 不穩定 (Unstable) | | 合併排序 | Merge Sort | O(nlogn) | O(nlogn) | 穩定 (Stable) | | 快速排序 | Quick Sort | O(nlogn) | O(n^2) | 不穩定 (Unstable) | | 二元樹排序 | Binary Tree Sort | O(nlogn) | O(n^2) | 穩定 (Stable)\* | | 堆積排序 | Heap Sort | O(nlogn) | O(nlogn) | 不穩定 (Unstable) | | 希爾排序 | Shell Sort | \*難以定論 | O(n^2) \*難以定論 | 不穩定 (Unstable) | | 基數排序 | Radix Sort | O(nk) | O(nk) | 穩定 (Stable) | **備註:** - \* 二元樹排序的穩定性取決於插入元素時的處理方式,若在插入相等元素時保持其相對順序,且使用中序遍歷表示,則可視為穩定排序。 - 基數排序的時間複雜度 O(nk) 中,n 為元素數量,k 為最大元素的位數。若 k 很大,則基數排序的效率可能不如其他 O(nlogn) 的排序算法。 - 希爾排序的時間複雜度難以定論,因為其效率高度依賴於所選擇的間隔序列(gap sequence)。 ## 搜尋 Searching Algorithms **搜尋算法是用於在資料結構中查找特定元素的過程。** - ### 線性搜尋 Linear Search - **概念:** 適用於未排序的資料結構,從資料結構的第一個元素開始,逐一比較每個元素,直到找到目標元素或遍歷完整個資料結構。 - ### 二元搜尋 Binary Search - **概念:** 適用於已排序的資料結構,通過反覆將搜尋範圍縮小一半來查找目標元素。 - ### 插值搜尋 Interpolation Search - **概念:** 適用於均勻分布的已排序資料結構,根據目標元素與資料範圍的比例來估算下一個搜尋位置。 - ### 跳躍搜尋 Jump Search - **概念:** 適用於已排序的資料結構,通過跳躍固定步長來快速縮小搜尋範圍,然後進行線性搜尋。 # 圖 Graphs **圖是一種由節點(vertices/nodes)和邊(edges)組成的資料結構,用於表示物件之間的關係。** ## 無向圖 Undirected Graph ### 定義 Definition 一個圖 G 可以表示為一對 G = (V, E),其中: - V(G) 是節點的集合,稱為頂點集(vertex set),每個節點代表一個物件。 - E(G) 是邊的集合,稱為邊集(edge set),每條邊連接兩個節點,表示它們之間的關係。 下圖為一個包含 5 個節點和 6 條邊的無向圖,其中: - V(G) = {A, B, C, D, E} - E(G) = {(A, B), (A, D), (B, C), (B, D), (C, E), (D, E)} ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-73.png) ### 術語 Terminology - **鄰接節點 Adjacent Nodes/neighbors:** 對每條邊來說,其中 e = (u, v) 連接節點 u 和 v,則 u 和 v 是彼此的鄰接節點。 - **度 Degree:** 節點 u 的度 deg(u) 是指與節點 u 相連的邊的數量。若 deg(u) = 0,則稱節點 u 為孤立節點(isolated node)。 - **路徑 Path:** 在圖中,一條路徑是由一系列相鄰節點組成的序列,表示從一個節點到另一個節點的連接方式。路徑 P 可以表示為 P = (v1, v2, ..., vk),其中每對相鄰節點 (vi, vi+1) 都存在於邊集 E(G) 中。若 v1 = vk,則稱該路徑為閉路(closed path)。 - **簡單路徑 Simple Path:** 若路徑中沒有重複的節點,則稱該路徑為簡單路徑。(例外: 起點與終點可相同,形成閉路,稱為簡單閉路 closed simple path) ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-74.png) - **迴路 Cycle:** 若路徑的起點和終點是同一個節點,且路徑中至少包含一條邊,則稱該路徑為迴路,同於閉路。簡單迴路(simple cycle)是指除了起點和終點外,路徑中沒有重複的節點,同於簡單閉路。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-75.png) - **正則圖 Regular Graph:** 若圖中所有節點的度相同,則稱該圖為正則圖。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-76.png) - **連通圖 Connected Graph:** 若圖中任意兩個節點之間都存在路徑(無孤立節點),則稱該圖為連通圖。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-77.png) - **完全圖 Complete Graph:** 若圖中每對不同的節點之間都存在一條邊,則稱該圖為完全圖。且完全圖中節點數為 n 的圖,邊數為 n(n-1)/2。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-78.png) - **團 Clique:** 圖中的一個子集 S ⊆ V(G),若 S 中的每對不同節點之間都存在一條邊,則稱 S 為圖 G 的一個團。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-79.png) - **迴圈 Loop:** 若圖中存在一條邊連接節點 u 到自身,即 e = (u, u),則稱該邊為迴圈。 - **多重邊 Multiedge:** 若圖中存在多條邊連接同一對節點 u 和 v,即 e1 = (u, v) 和 e2 = (u, v),則稱這些邊為多重邊。 - **多重圖 Multigraph:** 若圖中允許存在迴圈和多重邊,則稱該圖為多重圖。 - **圖的大小 Size of a Graph:** 圖 G 的大小定義為其邊的總數,記為 |E(G)|。 - **關節點 Articulation Point:** 在連通圖中,若刪除節點 v 及其相關邊後,圖變為不連通,則稱節點 v 為關節點。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-80.png) - **雙連通圖 Biconnected Graph:** 若圖中不存在關節點,則稱該圖為雙連通圖。 - **橋 Bridge:** 在連通圖中,若刪除邊 e 後,圖變為不連通(Disconnected),則稱邊 e 為橋。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-81.png) ## 有向圖 Directed Graph ### 定義 Definition 一個有向圖 G 可以表示為一對 G = (V, E),其中: - V(G) 是節點的集合,稱為頂點集(vertex set),每個節點代表一個物件。 - E(G) 是有向邊的集合,稱為邊集(edge set),每條有向邊連接兩個節點,表示從一個節點指向另一個節點的關係。 下圖為一個包含 5 個節點和 6 條有向邊的有向圖,其中: - V(G) = {A, B, C, D, E} - E(G) = {(A, B), (A, D), (B, D), (C, B), (D, E), (E, C)}其中(u, v)表示有向邊從節點 u 指向節點 v(u -> v)。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-82.png) ### 術語 Terminology - **入度 Indegree:** 節點 v 的入度 indeg(v) 是指指向節點 v 的有向邊的數量。 - **出度 Outdegree:** 節點 u 的出度 outdeg(u) 是指從節點 u 指向其他節點的有向邊的數量。 - **度 Degree:** 節點 u 的度 deg(u) 是指與節點 u 相連的有向邊的總數,即 deg(u) = indeg(u) + outdeg(u)。 - **源點 Source Node:** 若節點 u 的入度 indeg(u) = 0 且 outdeg(u) > 0,則稱節點 u 為源點。 - **匯點 Sink Node:** 若節點 v 的出度 outdeg(v) = 0 且 indeg(v) > 0,則稱節點 v 為匯點。 - **吊墜節點 Pendant Node:** 若節點 w 的度 deg(w) = 1,則稱節點 w 為吊墜節點。 - **可及性 Reachability:** 在有向圖中,若存在一條有向路徑從節點 u 指向節點 v,則稱節點 v 可由節點 u 可及(reachable)。 - **平行/多重邊 Parallel/Multiedge:** 若有向圖中存在多條有向邊連接同一對節點 u 和 v,即 e1 = (u, v) 和 e2 = (u, v),則稱這些邊為平行/多重邊。 - **強連通有向圖 Strongly Connected Directed Graph:** 若有向圖中任意兩個節點 u 和 v 之間都存在有向路徑從 u 指向 v 且從 v 指向 u,則稱該有向圖為強連通有向圖。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-83.png) - **弱連通有向圖 Weakly Connected Directed Graph:** 若將有向圖中的所有有向邊視為無向邊後,所得的無向圖為連通圖,則稱該有向圖為弱連通有向圖。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-84.png) - **遞移閉包 Transitive Closure:** 在有向圖中,若存在有向路徑從節點 u 指向節點 v,則在遞移閉包中也存在一條直接的有向邊從 u 指向 v。換句話說,遞移閉包這張圖記錄了一點能不能走到另一點。 ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-85.png) ## 最小生成樹 Minimum Spanning Tree (MST) **生成樹是一個連通無向圖的子集,一張圖可以有很多生成樹。生成樹必須是連通(connected)、無向(undirected)、無迴圈(No Loop)的。** **而最小生成樹則是在所有可能的生成樹中,在為每條邊賦予權重(weight)的情況下,選擇總權重相等或是小於其他生成樹的生成樹。** **可能會有很多的最小生成樹,取決於邊的權重分配。若所有邊的權重皆相同,則所有生成樹皆為最小生成樹;若邊的權重皆不同,則最小生成樹為唯一。而對於沒有權重的圖,可以將所有邊的權重視為相同,則所有生成樹皆為最小生成樹。** ### 普林演算法 Prim's Algorithm **普林演算法是一種用於尋找最小生成樹的貪心算法,其核心思想是從一個節點開始,逐步擴展生成樹,直到包含所有節點為止。** **口訣: `找鄰居->選最小->加入樹`** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-86.png) ### 克魯斯克爾演算法 Kruskal's Algorithm **克魯斯克爾演算法是一種用於尋找最小生成樹的貪心算法,其核心思想是從所有邊(E(G))中選擇權重最小的邊,並將其加入生成樹中,直到包含所有節點為止。若該圖未連通,則會找到最小生成森林。** **口訣: `選小邊->加不通`** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-87.png) ### 通用規則 general formulation ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-88.png) **在上述兩種演算法中,都以各自的準則選擇所謂的安全邊(safe edge),即在不形成迴路的情況下,將邊加入生成樹中。** - **普林演算法:** 從生成樹的節點出發,選擇與生成樹相連且權重最小的邊作為安全邊。 - **克氏演算法:** 從所有邊中選擇權重最小的邊,若該邊連接的兩個節點不曾經被連結過,則將其加入生成樹中作為安全邊。 ## 圖的表示法 Graph Representation **圖可以透過多種方式來表示,在電腦中儲存圖有三種常見方式:** - **串列表示(Sequential Representation):** 使用鄰接矩陣(adjacency matrix)來表示圖,其中矩陣的行和列分別代表節點,矩陣中的元素表示節點之間是否存在邊。 - **鏈結表示(Linked Representation):** 使用鄰接串列(adjacency list)來表示圖,其中每個節點都有一個鏈接列表(Link List),用來儲存節點的鄰居。 - **鄰接多重列表表示(Adjacency Multilist Representation):** 算是鄰接串列的進階版。 ### 鄰接矩陣 Adjacency Matrix **鄰接矩陣以一個 n x n 的二維陣列來表示一個有 n 個節點的圖,其中矩陣中的元素表示節點 n1 和節點 n2 之間是否存在邊,存在記為 1,不存在記為 0。(因此也被稱作比特矩陣 bit matrix 或是布林矩陣 boolean matrix)** **一個一階的鄰接矩陣 A^1 表示圖中節點之間的直接連接關係,而更高階的鄰接矩陣 A^k 則表示節點之間是否可以透過 k 條邊連接起來。我們可以透過(A^1)^k 來計算。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-90.png) **鄰接矩陣還有一些推廣如下:** **`B^k = A^1 + A^2 + ... + A^k`。** **path matrix P 設定若存在從節點 i 到節點 j 的路徑,則 P[i][j] = 1,否則為 0。** 其他的一些鄰接矩陣: ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-89.png) ### 鄰接串列 Adjacency List **鄰接串列通常被用來儲存邊數少至中等的圖,在電腦記憶體中,鄰接矩陣更適合用來儲存稀疏圖(sparse graph),而鄰接串列則更適合用來儲存稠密圖(dense graph)。** 一些鄰接串列的範例: ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-91.png) ### 鄰接多重列表 Adjacency Multilist **鄰接多重列表是鄰接列表的改良,他以基於邊的形式儲存資料,而藉由這個資料反向推出頂點的鄰接關係。** 範例: ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-92.png) ## 圖的遍歷 Graph Traversal **圖的遍歷是指從圖中的一個節點開始,按照一定的規則訪問圖中的所有節點和邊的過程。常見的圖遍歷算法有深度優先搜尋(Depth-First Search, DFS)和廣度優先搜尋(Breadth-First Search, BFS)。** ### 廣度優先搜尋 Breadth-First Search (BFS) **使用隊列(queue)來實現,從起始節點開始,先訪問該節點的所有鄰接節點,然後再依次訪問這些鄰接節點的鄰接節點,直到所有節點都被訪問為止。** **在樹上對應到階序遍歷(level-order traversal)。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-93.png) ### 深度優先搜尋 Depth-First Search (DFS) **使用堆疊(stack)來實現,從起始節點開始,沿著一條路徑一直深入訪問節點,直到無法繼續深入為止,然後回溯到上一個節點,繼續訪問其他未被訪問的鄰接節點,直到所有節點都被訪問為止。** **在樹上對應到前序遍歷(pre-order traversal)。(樹的前序、中序、後序都可以透過 DFS 實現)** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-94.png) ## 最短路徑演算法 Shortest Path Algorithms **給定一張圖 G = (V, E),其中 V 是節點集合,E 是邊集合,每條邊 (u, v) 有一個權重 w(u, v)。最短路徑問題是尋找從起始節點 s 到目標節點 t 的路徑,使得該路徑上所有邊的權重總和最小。** ### 戴克斯特拉演算法 Dijkstra's Algorithm **戴克斯特拉演算法是一種用於尋找單源最短路徑的貪心算法,適用於邊權重非負的圖(有向圖或無向圖皆可)。其核心思想是從起始節點開始,將周遭的節點依照距離進行更新,並選擇距離最短的節點進行擴展,直到找到目標節點或所有節點的最短距離都被確定。** **口訣: `選最近->放入已知->更新鄰居`** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-95.png) **以此無向圖為例:** - 選定起點 D,放入已知集合 S={D}並標註為 0,更新鄰居 F = 5, B = 15, G = 23,而其他未知點保持無限大。 - 選擇距離最近的節點 F,放入已知集合 S={D, F}並標註為 5,更新鄰居 C = 14, G = 18 (18 < 23,更新為 18)。 - 選擇距離最近的節點 C,放入已知集合 S={D, F, C}並標註為 9,更新鄰居 A = 16。 - 選擇距離最近的節點 B,放入已知集合 S={D, F, C, B}並標註為 15,更新鄰居 E = 32,鄰居 A 有新的權重 20,但 20 > 原權重 16,故不更新。 - 選擇距離最近的節點 A,放入已知集合 S={D, F, C, B, A}並標註為 16,沒有新的可及的鄰居,不更新。 - 選擇距離最近的節點 G,放入已知集合 S={D, F, C, B, A, G}並標註為 18,更新鄰居 E = 29 (29 < 32,更新為 29)。 - 選擇距離最近的節點 E,放入已知集合 S={D, F, C, B, A, G, E}並標註為 29,此時所有節點均已放入已知集合,演算法結束。 ### 貝爾曼-福特演算法 Bellman-Ford Algorithm **貝爾曼-福特演算法是一種用於尋找單源最短路徑的動態規劃算法,適用於邊權重可以為負的圖(有向圖或無向圖皆可)。其核心思想是通過反覆鬆弛(relaxation)邊來更新節點的最短距離,直到所有節點的最短距離都被確定。** **演算法會返回一個布林值,表示是否存在從起點到其他節點的負權重迴路(negative-weight cycle)。若存在負權重迴路,則無法找到最短路徑,演算法返回 false;否則返回 true,並產生最短路徑以及節點各自的權重。** **口訣: `重複鬆弛->更新距離`** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-96.png) **以此有向圖為例:** - **首先列出所有邊及權重:** - (t, x) = 5 - (t, y) = 8 - (t, z) = -4 - (x, t) = -2 - (y, x) = -3 - (y, z) = 9 - (z, x) = 7 - (z, s) = 2 - (s, t) = 6 - (s, y) = 7 - **選定起點 s,初始化距離: d(s) = 0,其他節點距離皆為無限大。** 1. 第一次鬆弛: - 使用邊 (s, t): 更新 d(t) = 6 (s -> t) - 使用邊 (s, y): 更新 d(y) = 7 (s -> y) - 其他無限大,無意義 2. 第二次鬆弛: - 使用邊 (t, x): 更新 d(x) = 11 (6 + 5 = 11 < ∞) - 使用邊 (t, y): 無更新 (6 + 8 = 14 > 7) - 使用邊 (t, z): 更新 d(z) = 2 (6 + (-4) = 2 < ∞) - 使用邊 (x, t): 無更新 (∞ + (-2) = ∞ > 6) - 使用邊 (y, x): 更新 d(x) = 4 (7 + (-3) = 4 < 11) - 使用邊 (y, z): 無更新 (7 + 9 = 16 > 2) - 使用邊 (z, x): 無更新 (2 + 7 = 9 > 4) - 使用邊 (z, s): 無更新 (2 + 2 = 4 > 0) - 使用邊 (s, t): 無更新 (0 + 6 = 6 > 6) - 使用邊 (s, y): 無更新 (0 + 7 = 7 > 7) 3. 第三次鬆弛... **持續進行鬆弛直到完成 V-1 次(其中 V 為節點數量),並進入 check 階段,用以確認是否有負權重迴路。** ![alt text](https://raw.githubusercontent.com/rytlebsk/DS-2025/master/image-97.png) - check: - 使用邊 (t, x): 無更新 (4 < 2 + 5) - 使用邊 (t, y): 無更新 (7 < 2 + 8) - 使用邊 (t, z): 無更新 (-2 = 2 + (-4)) - 使用邊 (x, t): 無更新 (2 = 4 + (-2)) - 使用邊 (y, x): 無更新 (4 = 7 + (-3)) - 使用邊 (y, z): 無更新 (-2 < 7 + 9) - 使用邊 (z, x): 無更新 (4 < -2 + 7) - 使用邊 (z, s): 無更新 (0 = -2 + 2) - 使用邊 (s, t): 無更新 (2 < 0 + 6) - 使用邊 (s, y): 無更新 (7 = 0 + 7) **所有邊皆無法更新距離,表示不存在負權重迴路,成功找到最短路徑及各節點權重,演算法結束。** ### 比較 | 演算法 | 非負權重圖 | 負權重圖 | 負權重迴圈圖 | | ---------------------------------------- | ---------- | -------- | ------------ | | 戴克斯特拉演算法 Dijkstra's Algorithm | 支援 | 不支援 | 不支援 | | 貝爾曼-福特演算法 Bellman-Ford Algorithm | 支援 | 支援 | 支援 | **由此可知,貝爾曼-福特演算法較為通用,但在非負權重圖中,戴克斯特拉演算法通常更有效率。** **而二者所產生的最短路徑有可能不同,但是節點的權重最終會相同。** --- # [本學期結束]