# 演算法筆記
**把我開發專案時常用(也有不常用但覺得很屌的)的演算法記錄下來,雖然都很基礎但很好用(但還是函式庫香**
## 1. 排序 (Sorting)
### (1) 氣泡排序 (Bubble Sort)
**重複掃描序列**,依序比較相鄰的兩個元素,
若前一個比後一個大,就交換它們的位置。
重複多次,得的到遞增(或遞減)順序。
---
#### ● 時間複雜度
| 情況 | 複雜度 |
|:--|:--|
| 最佳情況 | $O(n)$(當資料已排序) |
| 平均情況 | $O(n²)$ |
| 最差情況 | $O(n²)$ |
#### ● 空間複雜度
$θ(1)$(原地排序,不需額外空間)
---
#### ● 範例 (cpp)
| 參數名稱 | 說明 | 型態 |
|:--|:--|:--|
| `array` | 要進行排序的目標陣列 | `int[]` |
| `array_len` | 陣列的長度(元素個數) | `int` |
```cpp=
for (int i=0; i< array_len-1; i++) {
// 到 array_len-1 就可以,最後一項會自動歸位
for (int j=0; j< array_len-i-1; j++) {
// 比大小,前項較大的話就交換
if (array[j]>array[j+1]) {
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
```
### (2) 合併排序法 (Merge sort)
**合併排序**是一種**分治法 (Divide and Conquer)** 的排序演算法。
它將陣列不斷分割成更小的部分,直到只剩一個元素,再將這些子陣列依序合併回來,在合併過程中完成排序。
**步驟**
1. 將原始陣列分為左右兩半
2. 遞迴地對左右子陣列進行合併排序
3. 將兩個排序好的子陣列合併成一個完整的有序陣列
---
#### ● 時間複雜度
$O(n \log n)$(最佳、平均、最差皆相同)
#### ● 空間複雜度
$O(n)$(需額外空間分離原始陣列)
#### ● 範例 (cpp)
| 參數名稱 | 說明 | 型態 |
|:--|:--|:--|
| `array` | 要進行排序的目標陣列 | `int[]` |
| `l` | 陣列左界(起始索引) | `int` |
| `m` | 陣列分界點(中間索引) | `int` |
| `r` | 陣列右界(結束索引) | `int` |
```cpp=
// 合併兩個已排序的子陣列
// 左半 [l, m],右半 [m+1, r]
void merge(int array[], int l, int m, int r) {
int n1 = m - l + 1; // 左半長度
int n2 = r - m; // 右半長度
// 暫存左右子陣列
int L[n1], R[n2];
for (int i = 0; i < n1; i++) L[i] = array[l + i];
for (int j = 0; j < n2; j++) R[j] = array[m + 1 + j];
// 合併過程
int i = 0, j = 0, k = l;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) array[k++] = L[i++];
else array[k++] = R[j++];
}
// 將剩餘元素複製回主陣列
while (i < n1) array[k++] = L[i++];
while (j < n2) array[k++] = R[j++];
}
// 主函數:遞迴分割 + 合併
void merge_sort(int array[], int l, int r) {
if (l < r) {
int m = l + (r - l) / 2; // 中點
merge_sort(array, l, m); // 排左半
merge_sort(array, m + 1, r); // 排右半
merge(array, l, m, r); // 合併
}
}
```
### (3) 快速排序法 (Quick Sort)
它透過選擇一個基準值 (pivot),將陣列分為「比 pivot 小」與「比 pivot 大」的兩部分,
然後遞迴地對兩側進行相同的操作,直到整個陣列完成排序。
---
#### ● 演算法步驟
1. 選擇一個元素作為 **pivot**(通常取中間或最後一個)
2. 將陣列中小於 pivot 的元素放左邊,大於 pivot 的放右邊
3. 對左右子陣列重複步驟 1–2(遞迴)
---
#### ● 時間複雜度
| 情況 | 複雜度 |
|:--|:--|
| 最佳情況 | $O(n \log n)$ |
| 平均情況 | $O(n \log n)$ |
| 最差情況 | $O(n^2)$(當資料幾乎有序時) |
#### ● 空間複雜度
- $θ( \log n)$ (遞迴呼叫堆疊深度)
---
#### ● 範例 (C++)
| 參數名稱 | 說明 | 型態 |
|:--|:--|:--|
| `array` | 要進行排序的目標陣列 | `int[]` |
| `low` | 區間起始索引 | `int` |
| `high` | 區間結束索引 | `int` |
```cpp=
// 分區函式:將陣列分成比 pivot 小與比 pivot 大的兩部分
int partition(int array[], int low, int high) {
int pivot = array[high]; // 選擇最右邊元素為 pivot
int i = low - 1; // i 為小於 pivot 區間的最後索引
for (int j = low; j < high; j++) {
if (array[j] <= pivot) {
i++;
swap(array[i], array[j]); // 將小於 pivot 的元素移到左邊
}
}
swap(array[i + 1], array[high]); // 把 pivot 放到正確位置
return i + 1; // 回傳 pivot 的最終位置
}
// 主函式:遞迴進行快速排序
void quick_sort(int array[], int low, int high) {
if (low < high) {
int pi = partition(array, low, high);
quick_sort(array, low, pi - 1); // 排左半
quick_sort(array, pi + 1, high); // 排右半
}
}
```
### (4) 插入排序法 (Insertion sort)
從第二項開始一直比自身前幾項,並在自身的值小於此序列的值時將此項插入其序列,否則不動,並繼續比下去。
#### ● 時間複雜度
| 情況 | 複雜度 |
|:--|:--|
| 最佳情況 | $O(n)$ (當資料幾乎有序時)|
| 平均情況 | $O(n^2)$ |
| 最差情況 | $O(n^2)$(當資料為倒過來的有序時 (國語不好(´・ω・`) |
#### ● 空間複雜度
- $θ(1)$ (原地排序,不需額外空間)
#### ● 範例 (C++)
| 參數名稱 | 說明 | 型態 |
|:--|:--|:--|
| `array` | 要進行排序的目標陣列 | `int[]` |
| `array_len` | 目標陣列的長度 | `int` |
```cpp=
void insertion_sort() {
for (int i = 1; i < array_len; i++) { // 從第二個元素開始
int key = array[i]; // 要插入的值
int j = i - 1;
// 向左找適合的位置
while (j >= 0 && array[j] > key) {
array[j + 1] = array[j]; // 向右移動
j--;
}
array[j + 1] = key; // 插入 key 到正確位置
}
}
```
## 2. 搜索 (Searching)
### (1) 線性搜索
直接遍歷所有資料
Very Easy
#### ● 時間複雜度
| 情況 | 複雜度 |
|:--|:--|
| 最佳情況 | $O(1)$ (目標資料在第一個時)|
| 最差情況 | $O(n)$(目標資料在最後一個時)|
#### ● 範例 (c++)
| 參數名稱 | 說明 | 型態 |
|:--|:--|:--|
| `t_value` | 目標數值 | `int` |
| `t_index` | 目標數值在目標陣列內的 index | `int` |
| `array` | 要進行排序的目標陣列 | `int[]` |
| `array_len` | 目標陣列的長度 | `int` |
```cpp=
int t_index = -1;
for (int i=0; i<array_len; i++) {
if (array[i] == t_value) {
t_index = i;
break;
}
}
if (t_index != -1) printf("目標值 t_value 位於 array 內的 index: %d", t_index);
else printf("在 array 內未找到目標值 t_value");
```
### (2) 二分搜 (Binary Search)
>此演算法只能在**已排序**的陣列上使用!
取中間 index比較index值數字的大小,若較小往小的地方跑,較大的話往大的地方跑
繼續下去直到找到目標index
#### ● 時間複雜度
| 情況 | 複雜度 |
|:--|:--|
| 最佳情況 | $O(1)$ (陣列中間的數值剛好是被搜索的數值)|
| 最差情況 | $O(\log n)$(嚴格來說是 $log_2 (n)$)|
#### ● 範例 (c++)
| 參數名稱 | 說明 | 型態 |
|:--|:--|:--|
| `t_value` | 目標數值 | `int` |
| `t_index` | 目標數值在目標陣列內的 index | `int` |
| `array` | 要進行排序的目標陣列 | `int[]` |
| `array_len` | 目標陣列的長度 | `int` |
```cpp=
int t_index = -1;
int right = arr_len-1;
int left = 0;
while (right >= left) {
// 中間 index
int middle = (right + left) / 2;
// 大於目標值,往右跑
if (array[middle] > t_value) left = middle - 1;
// 小於目標值,往左跑
else if (array[middle] < t_value) right = middle + 1;
// 等於目標值,輸出
else {
t_index = middle;
break;
}
}
if (t_index != -1) printf("目標值 t_value 位於 array 內的 index: %d", t_index);
else printf("在 array 內未找到目標值 t_value");
```
### (3) 插補搜尋 (Interpolation Search)
>此演算法只能在**已排序**的陣列上使用!
二分法的變體,根據目標值與區間比例預估位置,比二分搜尋更適合均勻分布的數列。
#### ● 時間複雜度
| 情況 | 複雜度 |
|:--|:--|
| 最佳情況 | $O(1)$ 目標剛好被第一次預估命中|
| 最差情況 | O($log(log (n))$) (資料均勻分布) ~ $O(n)$ (資料沒有均勻分布)|
#### ● 範例 (c++)
| 參數名稱 | 說明 | 型態 |
|:--|:--|:--|
| `t_value` | 目標數值 | `int` |
| `t_index` | 目標數值在目標陣列內的 index | `int` |
| `array` | 要進行排序的目標陣列 | `int[]` |
| `array_len` | 目標陣列的長度 | `int` |
```cpp=
int t_index = -1;
int right = array_len - 1;
int left = 0;
while (t_value >= array[left] && t_value <= array[right] && right >= left) {
int p_index;
// 插補預測 index
p_index = left + (t_value - array[left]) / ( array[right] - array[left]) * (right - left);
// 左右相遇,看最後一個 index 是不是目標值
if(left == right){
if(t_value == array[left]) return left;
else break;
}
// index 是目標值
if(t_value == array[p_index]) {
t_index = p_index;
break;
}
// 大於目標值,往右跑
else if(t_value > array[p_index]) left=p_index+1;
// 小於目標值,往左跑
else right = p_index-1;
}
if (t_index != -1) printf("目標值 %d 位於 array 內的 index: %d\n", t_value, t_index);
else printf("在 array 內未找到目標值 %d\n", t_value);
```
## 3. 圖論演算法 (Graph Algorithms)
>這裡都以"找最近的路線"為範例
>老實說我很少用到(只有解題時用到
### (1) 廣度優先搜索 BFS
由起點開始,**一層一層地擴散**,直到找到終點為止。
BFS 在無權重的地圖中能找到**最短路徑**。
#### ● 範例 (c++)
| 參數名稱 | 說明 | 型態 |
|:--|:--|:--|
| `map` | n×m 地圖 (0 為道路; 1 為障礙) | `int[][]` |
| `n`, `m` | 地圖行列大小 | `int` |
| `start` | 起點座標 (x, y) | `pair<int, int>` |
| `goal` | 終點座標 (x, y) | `pair<int, int>` |
```cpp=
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
struct Point {
int x, y;
};
int main() {
int n = 5, m = 6;
// 我們的地圖
vector<vector<int>> map = {
{0, 0, 0, 0, 1, 0},
{1, 1, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 1},
{0, 1, 1, 1, 0, 0},
{0, 0, 0, 1, 0, 0}
};
Point start = {0, 0};
Point goal = {4, 5};
// 動作 (如向上 (dx[3], dy[3])
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
// 紀錄走過的地方
vector<vector<bool>> visited(n, vector<bool>(m, false));
// 紀錄步數
vector<vector<int>> dist(n, vector<int>(m, -1));
// 要處裡的點位陣列
queue<Point> q;
// 把起點加進去
q.push(start);
visited[start.x][start.y] = true;
dist[start.x][start.y] = 0;
bool found = false;
while (!q.empty()) {
// 將要處理的點位
Point cur = q.front();
q.pop();
// 檢查到終點了沒
if (cur.x == goal.x && cur.y == goal.y) {
found = true;
break;
}
// 處理上下左右 4 個點
for (int i = 0; i < 4; i++) {
int nx = cur.x + dx[i];
int ny = cur.y + dy[i];
// 有沒有出界?
if (nx < 0 || ny < 0 || nx >= n || ny >= m) continue;
// 有沒有走過?
if (map[nx][ny] == 1 || visited[nx][ny]) continue;
// 標記
visited[nx][ny] = true;
// 紀錄步數
dist[nx][ny] = dist[cur.x][cur.y] + 1;
// 新增點位
q.push({nx, ny});
}
}
if (found) cout << "找到路線!最短距離為 " << dist[goal.x][goal.y] << " 步\n";
else cout << "無法到達終點\n";
return 0;
}
```
### (2) 深度優先搜索 DFS
依照指定的搜索順序,嘗試所有可能的路線,直到找到目標為止。
相比 BFS,DFS 會**一條路走到底**,適合用來找是否能到達某個位置或**所有可能的路徑**。
#### ● 範例 (c++)
| 參數名稱 | 說明 | 型態 |
|:--|:--|:--|
| `map` | n×m 地圖 (0 為道路; 1 為障礙) | `int[][]` |
| `n`, `m` | 地圖行列大小 | `int` |
| `start` | 起點座標 (x, y) | `int[]` |
| `goal` | 終點座標 (x, y) | `int[]` |
```cpp=
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
// 我們的地圖
int n = 5, m = 6;
vector<vector<int>> map = {
{0, 0, 0, 0, 1, 0},
{1, 1, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 1},
{0, 1, 1, 1, 0, 0},
{0, 0, 0, 1, 0, 0}
};
// 動作 (如向上 (dx[3], dy[3])
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
// 起點與終點
int start[2] = {0, 0};
int goal[2] = {4, 5};
// 紀錄走過的地方
vector<vector<bool>> visited(n, vector<bool>(m, false));
bool found = false;
void dfs(int x, int y) {
// 超出邊界或遇障礙物
if (x < 0 || y < 0 || x >= n || y >= m) return;
if (map[x][y] == 1 || visited[x][y]) return;
// 到達目標
if (x == goal[0] && y == goal[1]) {
found = true;
return;
}
visited[x][y] = true;
// 遞迴探索四個方向
for (int i = 0; i < 4; i++) {
dfs(x + dx[i], y + dy[i]);
// 若已找到就提前結束
if (found) return;
}
// 回溯(若要找所有路徑才需要)
visited[x][y] = false;
}
int main() {
dfs(start[0], start[1]);
if (found) cout << "找到路線!" << endl;
else cout << "無法到達目標。" << endl;
}
```
## 4. 字串處理演算法 (String Algorithms)
> 我常用在搜索欄上(但有很多函式庫可以處理了(´・ω・`)
> 雜湊 Hashing 跟要用到 dp 的 LCS 我還不熟
> 這邊都是字串搜索的演算法
>
### (1) Brute Force(暴力搜尋)
han 暴力,逐一筆對字串格式
#### ● 時間複雜度
平均 $O(n × m)$
#### ● 範例 (c++)
| 參數名稱 | 說明 | 型態 |
|:--|:--|:--|
| `text` | 備搜索的字串 | `string` |
| `t_str` | 要搜索的目標字串 | `string` |
```cpp=
void brute_force(string text, string t_str) {
int s_len = text.length();
int t_len = t_str.length();
for (int i=0; i<s_len - t_len +1; i++) {
int start_index = i, end_index;
bool match = true;
for (int j=0; j<t_len; j++) {
end_index = i+t_len-1;
if(text[i+j] != t_str[j]) {
match = false;
break;
}
}
if (match) {
printf("找到目標字串位於%d至%d", start_index, end_index);
}
}
}
```
### (2) Knuth–Morris–Pratt(KMP)演算法
當失配發生時,利用模式字串本身的資訊(LPS表),直接跳到下一個可能的匹配位置,而不用退回主字串。
**LPS 表的定義**
>對於模式字串中每個位置 i,
>lps[i] = 該位置前(包含 i)這段字串中,
>最長的「前綴 = 後綴」子字串的長度。
#### ● 時間複雜度
$O(n + m)$
#### ● 範例 (c++)
1. 預先建立 lps 陣列。
2. 用兩個指標:
**i** 指向主字串(text)
**j** 指向模式字串(pattern)
3. 若 **text[i] == pattern[j]**,兩者都往右移。
4. 若**不匹配**:
若 **j != 0** → 讓 j = lps[j - 1](利用之前的資訊跳轉)。
若 **j == 0** → 只移動 i。
```cpp=
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 建立 LPS 陣列
vector<int> buildLPS(const string& pattern) {
int m = pattern.length();
vector<int> lps(m, 0);
// 目前最長前綴後綴長度
int len = 0;
int i = 1;
while (i < m) {
if (pattern[i] == pattern[len]) {
len++;
lps[i] = len;
i++;
}
else {
if (len != 0) {
// 回到前一個可能的長度
len = lps[len - 1];
}
else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
// KMP 主搜尋函式
void KMP_search(const string& text, const string& pattern) {
int n = text.length();
int m = pattern.length();
vector<int> lps = buildLPS(pattern);
// i -> text, j -> pattern
int i = 0, j = 0;
while (i < n) {
if (text[i] == pattern[j]) {
i++;
j++;
}
// 找到匹配
if (j == m) {
printf("找到目標字串位於 %d 至 %d\n", i - j, i - 1);
// 繼續搜尋下一個
j = lps[j - 1];
}
else if (i < n && text[i] != pattern[j]) {
if (j != 0) j = lps[j - 1];
else i++;
}
}
}
int main() {
string text = "ABABDABACDABABCABAB";
string pattern = "ABABCABAB";
KMP_search(text, pattern);
return 0;
}
```
### (3) Boyer–Moore 演算法
我覺得最好用的演算法喔,他是由右往左比對。當發生不匹配時,它利用兩個規則:
1. **壞字元規則**(Bad Character Rule)
2. **好尾巴規則**(Good Suffix Rule)
這兩個規則讓它可以一次跳過多個字元,而不是只移動一格,在實際運用上比 KMP 快很多。
#### ● 時間複雜度
平均 $O(n)$
#### ● 範例 (c++)
```cpp=
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
#define NO_OF_CHARS 256 // ASCII 字元集大小
// 建立壞字元表
void buildBadCharTable(const string& pattern, vector<int>& badChar) {
int m = pattern.length();
// 預設所有字元為 -1(代表 pattern 中不存在)
fill(badChar.begin(), badChar.end(), -1);
// 紀錄 pattern 中每個字元最後出現的位置
for (int i = 0; i < m; i++) {
badChar[(unsigned char)pattern[i]] = i;
}
}
// Boyer–Moore 主搜尋函式
void boyer_moore_search(const string& text, const string& pattern) {
int n = text.length();
int m = pattern.length();
vector<int> badChar(NO_OF_CHARS);
buildBadCharTable(pattern, badChar);
// 模式字串起始位置在 text 中的偏移量
int shift = 0;
while (shift <= n - m) {
int j = m - 1;
// 從右往左比對
while (j >= 0 && pattern[j] == text[shift + j]) {
j--;
}
if (j < 0) {
printf("找到目標字串位於 %d 至 %d\n", shift, shift + m - 1);
// 將 pattern 往右移
shift += (shift + m < n) ? m - badChar[(unsigned char)text[shift + m]] : 1;
} else {
// 根據壞字元表計算要移動的距離
shift += max(1, j - badChar[(unsigned char)text[shift + j]]);
}
}
}
int main() {
string text = "ABAAABCD";
string pattern = "ABC";
boyer_moore_search(text, pattern);
return 0;
}
```