# Coding Note-Data Structure 此篇為針對資料結構之筆記整理,內容參考至Horowitx.E(2003), Fundamentals of data structure in C, 13th printing、AP325書籍及大學開放式課程。除統整學習內容及概念外,也整理常見問題的討論及剖析,希望透過此筆記綜整所學並精進程式能力! Author: Vincent Chuang 莊閔翔 ## Arrays and Structures ### Strings 定義:String是一個字符的陣列,以表示文字資料。字串可包含字母、數字、符號和空格等字符,通常用來處理和操作文字資料。字串S可寫為$S=s_0, s_1, s_2..., s_{n-1}$,$s_i$即為其中的字母,組成一有限大小的字串。 接下來的討論中,以C++的STL提供的std::string類型處理字串,包含了各種方法操作字串,如拼接、查找、替換和分割,如下列示範: ``` #include <iostream> #include <string> using namespace std; int main() { string str1 = "Hello"; string str2("World"); // Combining strings string combined = str1 + ", " + str2; // length(size) of string int length = str1.length(); //print out each character for(int i=0; i<length; i++){ cout<<str[i]<<" "; } //append and erase str1.append(" C++"); //append at the end string str4 = "Hello, World!"; str4.erase(5, 7); // Erase ", World" // Insert string inserted = str2; //World inserted.insert(3, "ABC"); //WorABCld, "ld" will be pushed to the back //Compare if (str1.compare(str2) == 0) cout << "Equal" << endl; else cout << "Different strings" << endl; //Substring string s = "abcdefghijk"; s.substr(2,5); //指的是從索引2開始的5個字元的子字串,即cdefg s.find('c'); //(注意'')回傳2 s.find("fgh") //回傳5 s.find('c',5) //從索引5開始往後找,回傳INTMAX(找不到) //Split String return 0; } ``` #### Pattern Matching 方法:利用基本Bruteforce方法在給定的字串中,搜尋符合條件的特定字串 Code: ``` #include <iostream> #include <string> using namespace std; int find(string &str, string &pat) { int ls = str.length() - 1; int lp = pat.length() - 1; int endmatch = lp; for (int start = 0; endmatch <= ls; endmatch++, start++) { if (str[endmatch] == pat[lp]) { int j = 0, i = start; while (j < lp && str[i] == pat[j]) { i++; j++; } if (j == lp) return start; } } return -1; } int main() { string str = "ababbaabaa"; string pat = "aab"; int ans = find(str, pat); if (ans != -1) cout << "Found at index: " << ans << endl; else cout << "Not found." << endl; return 0; } ``` 以上述例子而言,"aab"此字串即可在str的第五個位置開始找到。 #### Knuth-Morris-Pratt algorithm(KMP) KMP為在一個字串中搜尋特定目標字串的演算法,最大的優化即為Fail function-透過 LPS(Longest Prefix Suffix)陣列避免不必要的反覆運算,大幅提升字串搜尋效率。以下概述其原理及應用方法 1. Longest Prefix Suffix(LPS) 定義:最長可匹配前綴與後綴的長度 以目標序列CABCAA為例,可得下列表格: |位置| 0 | 1 | 2 | 3 | 4 | 5 | |--|--|--|--|--|--|--| |字元|C| A| B| C| A| A | |LPS| 0| 0 | 0 | 1 | 2 |0 | 以讀取至位置3為例,有前綴序列'C'及後綴可匹配的序列'C',且為最大長度,因此LPS值為1。同理,當讀取至位置4時,有最長的匹配序列'CA',因此LPS值為2。 LPS Code範例: ``` #include <bits/stdc++.h> using namespace std; // 計算 LPS (Longest Prefix Suffix) 陣列 vector<int> computeLPS(const string &pattern) { int m = pattern.length(); vector<int> lps(m, 0); // LPS 陣列初始化為 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; } ``` 2.搜尋符合字串 ``` // KMP 搜尋演算法 vector<int> KMPSearch(const string &text, const string &pattern) { int n = text.length(); int m = pattern.length(); vector<int> lps = computeLPS(pattern); vector<int> result; // 存儲所有匹配的起始索引 int i = 0, j = 0; // i 遍歷 text, j 遍歷 pattern while (i < n) { if (text[i] == pattern[j]) { i++, j++; } if (j == m) { // 完整匹配 pattern result.push_back(i - j); j = lps[j - 1]; // 繼續查找下一個匹配 } else if (i < n && text[i] != pattern[j]) { if (j != 0) { j = lps[j - 1]; // 部分匹配,跳過已匹配部分 } else { i++; // 完全沒有匹配,移動 i } } } return result; } int main() { string text = "ABABDABACDABABCABAB"; string pattern = "ABABCABAB"; vector<int> matches = KMPSearch(text, pattern); if (matches.empty()) { cout << "Pattern not found" << endl; } else { cout << "Pattern found at positions: "; for (int pos : matches) cout << pos << " "; cout << endl; } return 0; } ``` 上述兩函數確保 KMP 演算法的時間複雜度為 O(n + m),比傳統的暴力法BruteForce O(nm) 更高效,適合用於較複雜題目,以提升效率。 ### Matrix - Inverse Matrix #### LU分解 參考線性代數書籍後,以LU分解的方法來決定所求矩陣的反矩陣。如此一來,除了不用將整個計算分為決定行列式值及高斯消去法兩個部分,能大幅降低時間複雜度。以下是關於我採用的LU分解方法討論以及時間複雜度分析。 對於一個矩陣A而言,其LU分解是將它分解成一個下三角矩陣 L 與上三角矩陣 U 的乘積。利用$det⁡(AB)=det⁡(A)×det⁡(B)$的性質,即可利用矩陣L及U的行列式以確定A是否為可逆矩陣,再進到反矩陣的運算,時間複雜度即可降至$O(n^3)$。 Pseudo code ``` PROCEDURE LU_Inverse(A, n) L = I_n //identity matrix U = O_n //zero matrix FOR k FROM 0 TO n-1 DO FOR i FROM k TO n-1 DO sum = 0 FOR j FROM 0 TO k-1 DO sum = sum + L[i][j] * U[j][k] END FOR L[i][k] = A[i][k] - sum END FOR FOR j FROM k TO n-1 DO sum = 0 FOR i FROM 0 TO k-1 DO sum = sum + L[k][i] * U[i][j] END FOR IF L[k][k] == 0 THEN RETURN "singular irreversable" U[k][j] = (A[k][j] - sum) / L[k][k] END FOR END FOR Y = O_n //Zero matrix in the order of n X = O_n // LY = I FOR i FROM 0 TO n-1 DO Y[i][i] = 1 FOR j FROM 0 TO i-1 DO sum = 0 FOR k FROM 0 TO j-1 DO sum = sum + L[i][k] * Y[k][j] END FOR Y[i][j] = -sum / L[i][i] END FOR END FOR // UX = Y FOR i FROM n-1 TO 0 DO FOR j FROM 0 TO n-1 DO sum = 0 FOR k FROM i+1 TO n-1 DO sum = sum + U[i][k] * X[k][j] END FOR X[i][j] = (Y[i][j] - sum) / U[i][i] END FOR END FOR RETURN X END PROCEDURE ``` ## Stacks AND Queues 此段為針對兩種在圖論以及運算中常使用的資料結構,stack還有queue的基礎運作邏輯以及其功能簡介,最後也將提及兩者特性,並會在下篇關於BFS以及DFS中,更深入地討論兩者的應用。 ### Stacks 定義:在一個有序的陣列中,插入和刪除操作都是在陣列頂部(top)進行的,即為後進先出(LIFO,Last-in-First-Out)的邏輯,如下圖。 ![image](https://hackmd.io/_uploads/B17kwJkwC.png) Ref: Prohramiz, stack data structure 利用C++ STL創建一個stack資料結構,即可看出其LIFO的特性,如下: ``` #include <iostream> #include <stack> using namespace std; int main() { stack<int> stack; stack.push(1); //Adding elements into stack(top) stack.push(2); stack.push(3); stack.pop(); //Removing elements from the stack(top) stack.pop(); while (!stack.empty()) { cout << stack.top() <<" "; //checking the stack is empty or not stack.pop(); } } ``` 運行後的結果即為1,因為其上方的元素皆被移除掉,更多的功能包括複製stack、確認是否已滿或是確認其大小等。 ### Queues 定義:一個有序列表,其中所有插入的操作都在一端(back)進行,而所有刪除操作都在另一端(front)進行,即為FIFO(First-in-First-Out)的邏輯。 ![image](https://hackmd.io/_uploads/ryhNhJyDR.png) Ref: Bestprog, C++ queue general concepts. Way to implement the queue 同理,利用c++ stl創建一個queue的資料結構來演示其FIFO的特性 ``` #include <iostream> using namespace std; int main() { queue<int>Queue; Queue.push(1); //Enqueue, inseting elements Queue.push(2); Queue.push(3); Queue.pop(); //Dequeue, removing elements while(!Queue.empty()) { cout << Queue.front() << "\n"; Queue.pop(); } return 0; } ``` Ouput即先後為2以及3,除已經移除掉的第一個元素外,其餘依照其插入進queue的順序列印出。 #### Circular queue 簡介: 與Queue同樣遵守FIFO規則,不過在邏輯上將尾端與前端連接,形成環狀結構。這樣可以有效地重複利用已釋放的空間,避免 Queue 在陣列中因空間無法移動的浪費。 code: ``` #include <iostream> using namespace std; class CircularQueue { int *arr; int front, rear, size, capacity; CircularQueue(int k) { capacity = k; arr = new int[k]; front = -1; rear = -1; size = 0; } bool enqueue(int value) { if ((rear + 1) % capacity == front) { cout << "Queue is Full\n"; return false; } if (front == -1) front = 0; rear = (rear + 1) % capacity; arr[rear] = value; size++; return true; } bool dequeue() { if (front == -1) { cout << "Queue is Empty\n"; return false; } if (front == rear) { front = rear = -1; // Queue becomes empty } else { front = (front + 1) % capacity; } size--; return true; } void display() { if (front == -1) { cout << "Queue is Empty\n"; return; } int i = front; while (true) { cout << arr[i] << " "; if (i == rear) break; i = (i + 1) % capacity; } cout << "\n"; } }; int main() { CircularQueue cq(5); //capacity set as 5 cq.enqueue(10); cq.enqueue(20); cq.enqueue(30); cq.enqueue(40); cq.display(); cq.dequeue(); cq.dequeue(); cq.display(); //comply FIFO rule cq.enqueue(50); cq.enqueue(60); cq.display(); // 30 40 50 60 cq.enqueue(70); //display full since the size is 5 } ``` ## Graph Theory ### DFS(Depth First Search) 簡介: 顧名思義DFS演算法從起點之鄰居開始搜索,搜索到沒有鄰居為止才結束,因此DFS較常以遞迴方式呈現其巡訪過程。 演算法架構(參考至ap325 P.230) ``` 初始化所有node vi, 其中visit[v]={false}; DFS(s){ visit[s]=true; for each v in adj[s] do if(!visit[v]) then v.p = s; DFS(v); endif endfor return; } ``` 寶藏問題:(Consider邊之權重) ``` #include <iostream> #include <vector> #include <algorithm> using namespace std; const int N = 1000; // Adjust the size as needed vector<int> adj[N]; int w[N]; bool visit[N] = {false}; vector<int> parent[N]; // Declare parent vector for each node int dfs(int s) { int weight = w[s]; visit[s] = true; // Set s as visited for (int u : adj[s]) { if (!visit[u]) { parent[u].push_back(s); // Record the parent of u weight += dfs(u); } } return weight; // Visit all of the nodes } int main() { int n, m; cin >> n >> m; int total = 0; for (int i = 0; i < n; i++) { cin >> w[i]; total += w[i]; } for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; adj[v].push_back(u); // v's adjacent = u adj[u].push_back(v); } int sum = 0; for (int v = 0; v < n; v++) { if (!visit[v]) sum = max(sum, dfs(v)); } cout << sum; return 0; } ``` ### BFS(Breadth First Search) 前情提要: 握手原理 有n個人握手,每人均握手x次,握手總次數S為nx/2。 圖論描述:在一無向圖中G(V,E),deg(V)的總和為2E 演算法架構(參考ap325 p.227) ``` 初始化for all v, visit[v] = false; v.d為v到初始點s的距離、v.p為v在BFS tree的parent; BFS(G,s){ queue<int> Q; Q.push(s), s.d = 0, visist[s] = true, s.p = -1; while(!Q.empty()) v = Q.front(); Q.pop() //移出(FIFO) for each u in adj(v) if(!visit[u]) visit[u] = true; u.d = v.d +1; u.p = v; Q.push(u); endif endfor endwhile } ``` BFS example: ``` #include <bits/stdc++.h> using namespace std; #define N 10010 vector<vector<int>> adj(N); // Using adjacency list vector<bool> visit(N, false); vector<int> d(N, -1); // Initialize distances with -1 void bfs(int s) { int cnt = 0, total_d = 0; queue<int> Q; Q.push(s); visit[s] = true; d[s] = 0; while (!Q.empty()) { int v = Q.front(); Q.pop(); for (int i : adj[v]) { if (!visit[i]) { d[i] = d[v] + 1; visit[i] = true; Q.push(i); cnt++; total_d += d[i]; // Summing the distances } } } cout << cnt << " " << total_d << "\n"; } int main() { int n, m, s; cin >> n >> m >> s; for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; adj[a].push_back(b); } bfs(s); return 0; } ``` ### 拓樸排序(Topological Sorting) 簡介:適用於 DAG(有向無環圖),幫圖上的節點做線性排序的演算法。排序的條件為對於一DAG圖中,可將所有節點排成一序列,使得所有有向邊滿足起點排在終點前。 觀念 1. DAG圖中的入度 對於一節點v而言,入度的定義為通往v邊數的總和。 2. 走訪順序關係 Kahn's Algorithm方法-依序刪除節點 以入度為0的點開始排序,並將走訪過的點刪除。因為拓樸排序試用於DAG,無環於圖中,因此刪除節點後,必有入度為0的點,再以此類推排序即可滿足條件。 Code: ``` vector<int> in_degree(1005, 0); vector<int> topo_order; void topological_sort(int n) { queue<int> q; for (int i = 0; i < n; i++) { if (in_degree[i] == 0) q.push(i); //考慮入度為0的點 } while (!q.empty()) { int node = q.front(); q.pop(); //將走訪過的節點刪除 topo_order.push_back(node); for (int neighbor : adj[node]) { if (--in_degree[neighbor] == 0) q.push(neighbor); } } } ``` ### 強連通分量(SCC, Strongly Connected Components) 定義:在一個強連通分量(SCC)中,圖中任兩點均有大於一種路徑可抵達,即不論從任意節點開始,都可以走完全部的點。 Ref:[ https://hackmd.io/@erichung0906/HkjZBH_IK](https://) #### Kosaraju Algorithm 運算邏輯及簡介: 以dfn, up 及stk來判斷是否符合SCC條件。dfn定義為dfs遍歷所走過的順序、low代表節點 u 或其子樹可回溯到的最小 dfn 值(代表是否能往上繞)、Stk來儲存要加入SCC的節點並依序push in。 當 low[u] == dfn[u] 時,表示從節點 u 開始,無法再往上繞回更前面的節點,這就是一個 SCC 的 root,即可開始滿足環繞所有 SCC 的路徑起點,可以從 stack 中彈出所有屬於這個 SCC 的節點。 Code: ``` #include <bits/stdc++.h> using namespace std; const int N = 1e4 + 5; vector<int> graph[N]; int dfn[N], low[N], timestamp = 0; bool in_stack[N]; stack<int> stk; int scc_count = 0; void tarjan(int u) { dfn[u] = low[u] = ++timestamp; stk.push(u); in_stack[u] = true; for (int v : graph[u]) { if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if (in_stack[v]) { low[u] = min(low[u], dfn[v]); } } if (dfn[u] == low[u]) { cout << "SCC " << ++scc_count << ": "; while (true) { int x = stk.top(); stk.pop(); in_stack[x] = false; cout << x << " "; if (x == u) break; } cout << "\n"; } } int main() { int n, m; cin >> n >> m; // 節點數與邊數 while (m--) { int u, v; cin >> u >> v; graph[u].push_back(v); } for (int i = 0; i < n; ++i) if (!dfn[i]) tarjan(i); return 0; } ``` #### Tarjan Algorithm 運算邏輯及簡介: 運用兩次DFS,分別進行順向與反向的遍歷。因為一個SCC不管是正向圖或是反向圖都不會影響其性質,所以正反dfs都可達到的點群即為SCC。在第一個dfs時,要找到最上游(圖左邊)的節點編號,依序往下游(圖右邊)排列,Stack的Top也就是Root,這樣才能在第二個dfs時,從下游(正向圖上游)開始建立SCC,讓在之後建立SCC時能把更下游的節點(已被建立為SCC的節點)篩掉。 Code: ``` #include <bits/stdc++.h> using namespace std; const int N = 1e4 + 5; vector<int> graph[N], rev_graph[N]; bool visited[N]; stack<int> stk; // record the order of visited nodes void dfs1(int u) { visited[u] = true; for (int v : graph[u]) { if (!visited[v]) dfs1(v); } stk.push(u); } void dfs2(int u) { visited[u] = true; cout << u << " "; for (int v : rev_graph[u]) { if (!visited[v]) dfs2(v); } } int main() { int n, m; cin >> n >> m; // nodes and edges for (int i = 0; i < m; ++i) { int u, v; cin >> u >> v; graph[u].push_back(v); // original rev_graph[v].push_back(u); // reverse graph } // Step 1: 正向 DFS 排序 memset(visited, false, sizeof(visited)); for (int i = 0; i < n; ++i) if (!visited[i]) dfs1(i); // Step 2: 反向 DFS 根據 stack 順序找 SCC memset(visited, false, sizeof(visited)); int scc_count = 0; while (!stk.empty()) { int u = stk.top(); stk.pop(); if (!visited[u]) { cout << "SCC " << ++scc_count << ": "; dfs2(u); cout << "\n"; } } return 0; } ``` ## C++ STL 簡介:C++ 標準模板庫(Standard Template Library, STL)是泛型類別和函數,可在程式中提供常用數據結構和算法。 ### Container ex: vector, list, set, map, queue, stack , set, deque #### vector Basic usages: ``` vector<int> v(size, initial_value); v.push_back(); //add element v.at(index); //access element v.pop_back(); //remove last elemnt v.size(); //number of element v.capcity(); //capacity of the container v.clear(); //remove all elements v.empty(); //check empty or not(in bool) ``` ### Algorithm ex: sort, find, count, for_each STL的algorithm中,最常見的即是以function形式存在(詳細sort的應用可見[https://hackmd.io/V3chzIpITnO0lyZu2TqUdg](https://)中排序演算法的部分) ### Iterator(疊代器) 簡介:Iterator 是 STL 中用來遍歷容器(如 vector、list、set)元素的工具,類似於指標,可以指向元素,並透過運算子如 *, ++, -- 來進行存取與移動。