###### tags: `資訊科技`
# 資料結構與演算法(使用Python)
## 一、學習資料結構與演算法的目的
1. ### 資料結構討論「資料儲存的方式」
- 如陣列、鏈結串列
2. ### 演算法討論「解決問題的想法」
- 如排序方法、找質數的方法
:mega: 所選用的資料結構和演算法將影響程式的複雜度與效率
3. ### 參考網站
- [30天學演算法和資料結構](https://ithelp.ithome.com.tw/users/20111557/ironman/2110)
- [演算法與資料結構](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/book/preface.html)
- [以Python實作資料結構](https://super9.space/archives/1105)
- [以Python實作演算法](https://super9.space/archives/1562)
- [The Algorithms - Python](https://github.com/TheAlgorithms/Python?fbclid=IwAR3bq27Qs_j6Qd21aKppbhcHALiQLsJ6v9pLORhru2xeEereV2F0R2ugToA)
<br />
## 二、資料結構
1. ### 堆疊
- LIFO(last in first out)
:::info
EX_01:10進位轉2進位(以list模擬)
:::
``` javascript=
stk = []
n = int(input('輸入n:'))
while ........ : # n不為0時繼續做
........ # 將n除以2的餘數push進堆疊(使用list的append)
........ # 將n/2
while len(stk) > 0: # 堆疊內還有元素
print(........ , end='') # 印出堆疊最上面的元素。pop沒有指定index,則傳回最後一個元素並刪除
```
2. ### 佇列
- FIFO(first in first out)
- 套件中的常用方法 (q = queue.Queue( ))
| 方法名稱 | 意義 |
|:----------|:---------------|
| q.qsize() | 回傳佇列大小 |
| q.empty() | 佇列是否為空 |
| q.put(x) | 將元素加入佇列後端|
| x=q.get() | 取出佇列前端的元素|
<br />
:::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=
from queue import Queue
q=Queue()
n = int(input('請輸入牌數:'))
for i in range(1,n+1):
........ # 將牌的編號(1 2 3 …)依序push(put)入佇列
while ........ : # 當佇列size>1時繼續執行
f=q.get() # 取出最上面的那張牌
........ # 取出次張牌,並將其重新放入佇列尾端
print(q.get()) # 印出剩下的那張牌
```
:::info
EX_02 (進階):計算迷宮的最短路徑。
[2維陣列-列表生成式、串列綜合表達式(list comprehensions)](https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431779637539089fd627094a43a8a7c77e6102e3a811000)
:::
``` javascript=
from queue import Queue
len = [[0]*10 for _ in range(10)]
# len為2維陣列,記錄起點到每一點的長度,列表生成式。
maze = [[1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,1],
[1,1,0,1,0,1,1,1,1,1],
[1,0,0,1,0,1,0,0,0,1],
[1,0,1,1,0,1,1,0,0,1],
[1,0,1,0,0,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1]]
# 陣列最外層包一圈牆。(1,1) 為起點,(8,8)為終點。
q = Queue()
q.put((1, 1)) # 先把起點放入佇列
len[1][1] = 1
maze[1][1] = 1 # 走過設為牆
while not q.empty():
now=q.get() # 從佇列開頭取一個點
x,y=now[0],now[1] # 此點座標置於x,y
if maze[x + 1][y + 0] == 0: # 此點的右方有路
nx = x + 1 # 下一點座標
ny = y + 0
q.put((nx,ny)) # 下一個點放入佇列
len[nx][ny] = len[x][y] + 1 # 長度+1
maze[nx][ny] = 1 # 走過設為牆
if maze[x + 0][y + 1] == 0: # 此點的下方有路
nx = x + 0
ny = y + 1
q.put((nx,ny))
len[nx][ny] = len[x][y] + 1;
maze[nx][ny] = 1
if maze[x - 1][y + 0] == 0: # 此點的左方有路
........
........
........
........
........
if ........ : # 此點的上方有路
........
........
........
........
........
'''
x,y=q.get()
for nx,ny in [(x+1, y), (x, y+1), (x-1, y), (x, y-1)]:
if maze[nx][ny] == 0:
q.put((nx,ny)) # 下一個點放入佇列
len[nx][ny] = len[x][y] + 1 # 長度+1
maze[nx][ny] = 1;
'''
if len[8][8] == 0:
print('沒有路!')
else:
print('最短路徑為 ', len[8][8])
```
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)
* 前序走訪(preOrder):先走訪樹根,然後左子樹,最後右子樹。
* 中序走訪(inOrder):先走訪左子樹,然後樹根,最後右子樹。
* 後序走訪(postOrder):先走訪左子樹,然後右子樹,最後樹根。
<br/>
:::info
EX_03:下圖的中序、前序、後序走訪分別為?
![](https://i.imgur.com/J1qqaQ1.png =350x)
:::
``` javascript=
data=[0,1,2,3,4,5,6]
def preorder(i): # 前序
if(i>=7):
return;
else:
print(data[i], end=' ')
preorder(i*2+1)
preorder(i*2+2)
'''
if(i<7):
print(data[i], end=' ')
preorder(i*2+1)
preorder(i*2+2)
'''
........ # 中序
........ # 後序
preorder(0)
print('\n')
inorder(0)
print('\n')
postorder(0)
print('\n')
```
- [二元搜尋樹](http://marklin-blog.logdown.com/posts/1526463)
:::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)
* [霍夫曼編碼(Huffman Coding)](http://puremonkey2010.blogspot.com/2011/02/huffman-code.html)
:::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)
* 相鄰串列表示法(Adjacency Lists)
- 圖的搜尋
* [深度優先搜尋(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)
:::
:::info
EX_06_2:[ZeroJudge c129: 00572 - Oil Deposits](https://zerojudge.tw/ShowProblem?problemid=c129)。
以DFS找出@相鄰土地有@有幾塊。
:::
``` javascript=
r,c = map(int, input().split())
visited = [[0]*c for _ in range(r)]
map=[]
for i in range(r):
line=list(input())
map.append(line)
def dfs(i, j):
if i<0 or i>=r or .... or .... or .... or .... : # i(列)、j(欄)超出邊界、此點已被拜訪過、此點沒油
return
visited[i][j]=1
dfs(i-1,j-1) # 左上。螢幕左上角為原點,往下、往右為正。 先看列(數學上的y),後看欄(數學上的x)
dfs(i-1,j) # 上
dfs(...., ....) # 右上
........ # 左
........ # 右
........ # 左下
........ # 下
........ # 右下
cnt=0
for ........ # 第 i 列
for ........ # 第 j 欄
if ........ # 目前座標沒被拜訪過 而且 有油
dfs(.... , ....) # 從此點開始DFS
cnt+=1
print(cnt)
# 地圖、visited陣列輸出
# Bonus:visited陣列以編號標出不同的油田
測資(ans=4)
9 10
@@*@*@@*@@
@@*@**@*@*
*@@@****@@
*@@@**@**@
***@*@****
@@@@@*@*@@
@*@@**@@@*
@*@*******
@***@@@@**
```
:::info
EX_06 (Bonus):[ZeroJudge d626: 小畫家真好用](https://zerojudge.tw/ShowProblem?problemid=d626)。
以DFS找出油漆桶能上色的範圍。
:::
:::info
EX_06 (進階):以DFS,計算EX_02_2(進階)迷宮的最短路徑。
:::
``` javascript=
def dfs(sx, sy, step):
if (sx == gx and sy == gy):
global MIN
if(step < MIN):
MIN = step
return
for nx,ny in [(sx+1, sy), (sx, sy+1), (....), (....)]: # 下一個點的順序:右、下、左、上
if maze[nx][ny] == 0 and visited[nx][ny] == 0:
visited[nx][ny] = 1 # 下一個要走的點先設為走過
........ # 遞迴呼叫實作DFS,從nx,ny繼續往後走,走的步數+1
........ # 下一個要走的點恢復為沒走過
return
MIN = 999
visited = [[0]*10 for _ in range(10)]
maze = [[1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,1],
[1,1,0,1,0,1,1,1,1,1],
[1,0,0,1,0,1,0,0,0,1],
[1,0,1,1,0,1,1,0,0,1],
[1,0,1,0,0,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1]]
# 陣列最外層包一圈牆。(1,1) 為起點,(8,8)為終點。
sx,sy = 1,1 # 起點
gx,gy = 8,8 # 終點
visited[1][1] = 1 # 將起點標記為1,表示走過,避免重復走。
dfs(sx, sy, 1)
print(f'最短路徑:{MIN}')
```
- 圖的應用
* [最短路徑演算法(一個頂點到多頂點-代克思托Dijkstra)](http://puremonkey2010.blogspot.com/2013/05/alg-info-dijkstras-algorithm-shortest.html)
:::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)
(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:求3萬內的質數。
:::
``` javascript=
import datetime
start = datetime.datetime.now()
n=30000
lst=[]
for i in range(2, n+1):
prime = True
for j in range(2, i): # 測試2~i-1是否有因數
if i%j == 0:
prime = False
break
if prime:
lst.append(i)
print(lst)
end=datetime.datetime.now()
print(end-start) # 11.23sec
```
``` javascript=
import datetime
start = datetime.datetime.now()
n=30000
lst=[]
for i in range(2, n+1):
for j in range(2, i): # 測試2~i-1是否有因數
if i%j == 0:
break
else: # for-else,for循環完整完成才執行else
lst.append(i)
print(lst)
end=datetime.datetime.now()
print(end-start) # 9.72sec
```
:::info
EX_09_2:求20萬內的質數。
:mega:
1. 如果一個數是合數,它的最小質因數會小於等於它的平方根。
2. 因為成對出現的因數中,一個會≦√n,而另一個會≧√n。因此只要檢查≦√n中是否有n的因數即可。
:::
``` javascript=
import datetime
start = datetime.datetime.now()
n=200000
lst=[]
for i in range(2, n+1):
for j in range(2, int(i**0.5)+1): # 測試2~√i是否為因數
if i%j == 0:
break
else: # for-else,for循環完整完成才執行else
lst.append(i)
print(lst)
end=datetime.datetime.now()
print(end-start) # 1.93sec
```
:::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),求100萬內的質數。
:::
``` javascript=
import datetime
start = datetime.datetime.now()
n=1000000
prime = [True]*(n+1) # 產生n+1個True的list
for i in range(2, int(n**0.5)+1): # 檢查到<=√n即可
if prime[i]:
for j in range(...., n+1, ....): # 如果i是質數,則篩掉它的倍數。(從i^2開始,每次+i)
prime[....] = .... # i的倍數不為質數
lst = [x for x in range(2, n+1) if prime[x]]
print(lst)
end=datetime.datetime.now()
print(end-start) # 0.37sec
```
<br />
2. ### 分而治之(Divid and Conquer)
- [合併排序(Merge Sort)](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=
def dc(b,p,m):
if ........ # p分割到1時停止
return ........
else:
if ........
return ........ # p為偶數,( b^(p/2)%m * b^(p/2)%m ) % m
else:
........ # p為奇數要比偶數時多乘一次b
b,p,m = map(int,input("請輸入b、p、m(空白隔開): ").split())
print(dc(b,p,m))
```
:::info
EX_12 (進階):合併排序實作。
:::
``` javascript=
def mergesort(lst):
if len(lst) <= 1:
return lst
mid = len(lst)//2
left = mergesort(lst[:mid])
right = mergesort(lst[mid:])
mrg_lst = []
while left and right: # 即while len(left)>0 and len(right)>0: 因為空的串列即為False,可直接邏輯判斷
if left[0] < right[0]:
mrg_lst.append(left.pop(0)) # 傳回left索引值0的元素,並將其刪除
else:
mrg_lst.append(right.pop(0))
mrg_lst += left + right # left 或 right剩下的元素全部加到mrg_lst
return mrg_lst
lst=[6,3,5,1,8,2,7,4]
print(mergesort(lst))
```