###### tags: `資訊科技` # 資料結構與演算法(使用C++) ## 一、學習資料結構與演算法的目的 1. ### 資料結構討論「資料儲存的方式」 - 如陣列、鏈結串列 2. ### 演算法討論「解決問題的想法」 - 如排序方法、找質數的方法 :mega: 所選用的資料結構和演算法將影響程式的複雜度與效率 3. ### 參考網站 - [演算法與資料結構](http://alrightchiu.github.io/SecondRound/mu-lu-yan-suan-fa-yu-zi-liao-jie-gou.html) - [Algorithms and Data Structures](http://gousios.org/courses/algo-ds/) <br /> ## 二、資料結構 1. ### 堆疊 - LIFO(last in first out) - [[C++] STL 容器 (一) - 基本介紹](http://larry850806.github.io/2016/06/06/STL1/) - C++ 堆疊(Stack)常用的方法 | 名稱 | 意義 | |:-----|:----------| |stack<int> sk |建立一個int型別的stack| |sk.size() |堆疊元素數量| |sk.empty() |判斷堆疊是否為空| |sk.push(x) |從堆疊「頂端」加入一個元素x| |sk.pop() |刪除堆疊頂端元素| |sk.top() |取得堆疊頂端元素| :::info EX_01:10進位轉2進位(以STL stack實作)。11->1011 ::: ``` javascript= int main() { stack<int> sk; // 需含入 stack 標頭檔 int n; cin >> n; while (....) // n大於0 { ........ // 將n除以2的餘數push進堆疊 ........ // 將n除以2 } while (........) // 堆疊內還有元素 { cout << ........ // 印出堆疊頂端元素 ........ // 將堆疊頂端元素移除 } } ``` 2. ### 佇列 - FIFO(first in first out) - C++ 佇列(Queue)常用的方法 | 名稱 | 意義 | |:--------|:----------| |queue<int> q |建立一個int型別的queue| |q.size() |佇列元素數量| |q.empty() |判斷是否為空| |q.push(x) |從佇列「後端」加入一個元素x| |q.pop() |從「前端」刪除一個元素| |q.front() |取得前端元素| |q.back() |取得後端元素| :::info EX_02:[ZeroJudge e183: 10940 - Throwing cards away II](https://zerojudge.tw/ShowProblem?problemid=e183) 給你一疊有1~n編號的牌(1在最上面,n在最下面),在牌數大於1的時候執行以下操作:「丟掉最上面的牌,並把現在最上面的的牌放到最下面。」 求最後剩下的那張牌編號為?(7->6、10->4) ::: ``` javascript= int main() { ........ // 宣告一個int型別的queue,名為q,需含入 queue 標頭檔 int n; cin >> n; for (int i=1;i<=n;i++) ........ // 將牌的編號(1 2 3 …)依序push入佇列 while ( ........ ) // 當佇列size>1時繼續執行 { ........ // 將最上面的牌丟掉 ........ // 將次張牌重新放入佇列尾端 ........ // 將次張牌丟掉 } cout << ........ // 印出剩下的那張牌 return 0; } ``` 3. ### 樹 - [模擬具有樹枝狀性質的資料](http://www2.csie.ntnu.edu.tw/~u91029/Tree.html),由節點與邊組成,例如家族族譜、電腦資料夾結構。 ![](https://i.imgur.com/sbgxlao.png =100x) - [二元樹](https://mark-lin.com/posts/20170309/#--binary-tree-) - [二元樹的走訪(Tree Traversal)](http://alrightchiu.github.io/SecondRound/binary-tree-traversalxun-fang.html#pre)(課本p4-7) * 前序走訪(preOrder):先走訪樹根,然後左子樹,最後右子樹。 * 中序走訪(inOrder):先走訪左子樹,然後樹根,最後右子樹。 * 後序走訪(postOrder):先走訪左子樹,然後右子樹,最後樹根。 <br/> :::info EX_03:下圖的前序、中序、後序走訪分別為?(先以人工走訪,再用程式驗証) ![](https://i.imgur.com/J1qqaQ1.png =350x) ::: ``` javascript= #include<iostream> using namespace std; int data[7]={0,1,2,3,4,5,6}; // 全域變數 void preorder(int i) // 前序,void表示沒有回傳值 { if(i>=7) return; else { cout << data[i] << " "; preorder(2*i+1); preorder(2*i+2); } /* if(i<7) { cout << data[i] << " "; preorder(i*2+1); preorder(i*2+2); } */ } void inorder(int i) // 中序 { if(i>=7) return; else { ........ ........ ........ } } ........ // 後序 { if ....... ....... else { ........ ........ ........ } } int main(void) { preorder(0); cout << endl; inorder(0); cout << endl; postorder(0); cout << endl; } /* 前序(preOrder): 中序(inOrder): 後序(postOrder): */ ``` - [二元搜尋樹](https://mark-lin.com/posts/20170309/#--binarysearch-tree-)(課本p4-12) :::info EX_04:建立二元搜尋樹 依讀入順序 30, 15, 50, 35, 10, 25, 40, 20, 45 建立一個二元搜尋樹(binary search tree)。 ::: - 樹的應用 * [遊戲樹(game tree)](https://commons.wikimedia.org/wiki/File:Tic-tac-toe-full-game-tree-x-rational.jpg)(課本p4-20) * [霍夫曼編碼(Huffman Coding)](http://puremonkey2010.blogspot.com/2011/02/huffman-code.html)(課本p4-24) :::info EX_05:若一篇文章每個字母出現的次數為,a:8,b:7,c:3,d:4,e:5,建立Huffman Tree(次數小的結點放左邊),並求每個字母的Huffman Code。 a->11 b-> c-> d-> e->00 ::: 4. ### 圖 - [圖形是「頂點」(vertex)和邊(edge)所組成](https://mark-lin.com/posts/20170311/) - 圖常用的表示法 * [相鄰矩陣表示法(Adjacency Matrix)](http://web.ntnu.edu.tw/~algo/Graph.html#3)(課本p5-10) * 相鄰串列表示法(Adjacency Lists) - 圖的搜尋(課本p5-12) * [深度優先搜尋(Depth First Search,DFS)](https://jason-chen-1992.weebly.com/home/-graph-searching-methods):以堆疊(Stack)、遞迴來實作。(類似樹的前序走訪) * [廣度優先搜尋(Breadth First Search,BFS)](http://simonsays-tw.com/web/DFS-BFS/BreadthFirstSearch.html):以佇列(Queue)來實作。 :::info EX_06_1:下圖的DFS與BFS走訪順序為(子節點以字母序走訪)? ![](https://i.imgur.com/JGKRYVy.png =450x) DFS:A B C F H ... BFS:A B C D E ... ::: :::info EX_06_2:[ZeroJudge c129: 00572 - Oil Deposits](https://zerojudge.tw/ShowProblem?problemid=c129)。 以DFS找出@相鄰土地有@有幾塊。 ::: ``` javascript= #include <iostream> using namespace std; int visited[100][100]={0}; char mp[100][100]; int r,c; // 全域變數 void dfs(int i,int j) { if(i<0 || i>=r || .... || .... || .... || ....) // i(列)、j(欄)超出邊界、此點已被拜訪過、此點沒油 return; visited[i][j]=1; dfs(i-1, j-1); // 左上。螢幕左上角為原點,往下、往右為正。 先看列(數學上的y),後看欄(數學上的x) dfs(i-1, j); // 上 dfs(...., ....); // 右上 ........ // 左 ........ // 右 ........ // 左下 ........ // 下 ........ // 右下 } int main() { int cnt=0; cin >> r >> c; for(int i=0;i<r;i++) for(int j=0;j<c;j++) cin >> mp[i][j]; for ........ // 第 i 列 { for ........ // 第 j 欄 { if(........) // 目前座標沒被拜訪過 而且 有油 { dfs(.... , ....); // 從此點開始DFS cnt++; } } } cout << cnt << endl; // 地圖、visited陣列輸出 // Bonus:visited陣列以編號標出不同的油田 return 0; } 測資(ans=4) 9 10 @@*@*@@*@@ @@*@**@*@* *@@@****@@ *@@@**@**@ ***@*@**** @@@@@*@*@@ @*@@**@@@* @*@******* @***@@@@** ``` :::info EX_06 (Bonus):[ZeroJudge d626: 小畫家真好用](https://zerojudge.tw/ShowProblem?problemid=d626)。 以DFS找出油漆桶能上色的範圍。 ::: - 圖的應用 * [最短路徑(一個頂點到多頂點Dijkstra)](http://puremonkey2010.blogspot.com/2013/05/alg-info-dijkstras-algorithm-shortest.html)(課本p6-20) :::info EX_07:畫出下圖的相鄰陣列,以Dijkstra演算法,求A到各點的最短路徑(以Excel表格表示)?++**0 8 6 4 3 1 5**++ ![](https://i.imgur.com/f0wz9DD.jpg =270x) ::: * [最小生成樹(minimum spanning tree)](https://www.itread01.com/content/1545066724.html)(課本p5-23) (1). Kruskal演算法為一個使用貪婪法 (greedy method) 的演算法,一開始為一空樹,每次在不讓圖中含有任何迴路(cycle)下尋找圖中最小權重的邊加入樹中,直到所有點皆在樹中即完成。 (2). Prim演算法與Dijkstra演算法類似,屢次找出不在樹上,離樹最近的點。 :::info EX_08:畫出下圖的最小生成樹,其權重總和為?++**28**++ ![](https://i.imgur.com/tjw9GuN.png =350x) ::: <br /> ## 三、演算法 1. ### 演算法效能 - [運算能力](https://buzzorange.com/techorange/2019/09/11/solve-sums-of-three-cubes/) * 循序搜尋 v.s. 二分搜尋 - [時間複雜度](https://jason-chen-1992.weebly.com/home/time-space-complexity) * [常見的六種時間複雜度與演算法](https://medium.com/appworks-school/%E5%88%9D%E5%AD%B8%E8%80%85%E5%AD%B8%E6%BC%94%E7%AE%97%E6%B3%95-%E5%BE%9E%E6%99%82%E9%96%93%E8%A4%87%E9%9B%9C%E5%BA%A6%E8%AA%8D%E8%AD%98%E5%B8%B8%E8%A6%8B%E6%BC%94%E7%AE%97%E6%B3%95-%E4%B8%80-b46fece65ba5) - 求質數演算法效能比較 :::info EX_09_1:求10萬內的質數。註解18行(並在上一行加分號)->1.411 sec ::: ``` javascript= int main() { int n=100000; for (int i=2;i<=n;i++) { bool prime=true; for ........ // 測試2~i-1是否為其因數 { if ........ // 如果j為i的因數 { ........ // prime 設為 false ........ // 跳出此層迴圈 } } if ........ // 如果i不是質數 cout << i << " "; } return 0; } ``` :::info EX_09_2:求100萬內的質數。註解18行(並在上一行加分號)->0.902 sec :mega: 1. 如果一個數是合數,它的最小質因數會小於等於它的平方根。 2. 因為成對出現的因數中,一個會≦√n,而另一個會≧√n。因此只要檢查≦√n中是否有n的因數即可。 ::: ``` javascript= int main() { int n=1000000; for (int i=2;i<=n;i++) { bool prime=true; for (int j=2; ........ ;j++) // 測試2~√i是否為其因數,sqrt需含入cmath標頭檔 { if ........ // 如果j為i的因數 { ........ // prime 設為 false ........ // 跳出此層迴圈 } } if ........ // 如果i是質數 cout << i << " "; } return 0; } ``` :::info EX_10:[埃拉托斯特尼篩法](https://zh.wikipedia.org/wiki/%E5%9F%83%E6%8B%89%E6%89%98%E6%96%AF%E7%89%B9%E5%B0%BC%E7%AD%9B%E6%B3%95)(課本p3-5),求1000萬以內的質數。註解15行(並在上一行加分號)->0.155 sec ::: ``` javascript= int main() { int n=10000000; bool prime[10000001]; // 區域變數只能到200萬左右,改為全域變數。 fill(prime,prime+n+1,1); // 全部先設為1(true),假設都是質數。需含入algorithm標頭檔 for (int i=2;i<=sqrt(n);i++) // 檢查到<=√n即可 if ........ // 如果i是質數 for ........ // 如果i是質數,則篩掉它的倍數。(從i^2開始,每次+i) prime[....]=....; // i的倍數不為質數 for (int i=2;i<=n;i++) if ........ // 如果i是質數 cout << i << " "; return 0; } ``` <br /> 2. ### 分而治之(Divid and Conquer) - [合併排序(Merge Sort)1](https://medium.com/appworks-school/%E5%88%9D%E5%AD%B8%E8%80%85%E5%AD%B8%E6%BC%94%E7%AE%97%E6%B3%95-%E6%8E%92%E5%BA%8F%E6%B3%95%E9%80%B2%E9%9A%8E-%E5%90%88%E4%BD%B5%E6%8E%92%E5%BA%8F%E6%B3%95-6252651c6f7e)、[合併排序2](http://alrightchiu.github.io/SecondRound/comparison-sort-merge-sorthe-bing-pai-xu-fa.html) :::info EX_11:[ZeroJudge d219: 00374 - Big Mod](https://zerojudge.tw/ShowProblem?problemid=d219) 對相當大的b、p、m,請寫一個有效率的演算法來計算 r = b^p mod m ( 1 <= b、p、m <= 32767 ) b=2、p=8、m=10 → r=6 b=2、p=10、m=10 → r=4 b=17、p=1765、m=3 → r=2 ::: :mega: [分配律](https://zh.wikipedia.org/wiki/%E6%A8%A1%E9%99%A4):(A\*B)%M=[ (A%M)\*(B%M) ]%M→B^P^%M=[ (B^P/2^%M)\*(B^P/2^%M) ]%M EX:4\*5%3=(4%3)\*(5%3)、5\*8%3=(5%3)\*(8%3)%3 ``` javascript= int dc(int b,int p,int m) { if ........ // p分割到1時停止 return ........ else { if ........ return ........ // p為偶數,( b^(p/2)%m * b^(p/2)%m ) % m else ........ // p為奇數要比偶數時多乘一次b } } int main() { int b,p,m; while(cin >> b >> p >> m) cout << dc(b,p,m) << endl; return 0; } ```