數據結構——圖node
一、圖的基本概念python
二、圖的數據表示法算法
2.1 鄰接矩陣表示法數組
假設一個圖A有n個頂點,咱們以n*n的二維矩陣列來表示它,這個二維矩陣就是該圖的鄰接矩陣,此矩陣的定義以下:對於一個圖G=(V,E),假設有n個頂點,n>=1,則能夠將n個頂點的圖使用一個n*n的二維矩陣來表示,其中A(i,j)=1,則表示圖中有一條邊(Vi,Vj)存在,反之,A(i,j)=0,則不存在(Vi,Vj)。
網絡
相關特性說明以下:數據結構
(1)對無向圖而言,鄰接矩陣必定是對稱的,而對角線必定爲0。有向圖則不必定如此。app
(2)在無向圖中,任意結點 i 的度數就是第 i 行全部元素的和。在有向圖中,結點 i 的出度就是第 i 行全部元素之和;結點 j 的入度就是第 j 列全部元素之和。函數
(3)用鄰接矩陣法表示圖共須要 n^2 個單位空間,因爲無向圖的鄰接矩陣具備對稱關係的,扣除對角線所有爲 0 外,僅須要存儲上三角形數據便可,所以僅須要n(n-1)/2。工具
接下來,咱們看一個實際的例子,以鄰接矩陣來表示無向圖,無向圖以下圖所示:spa
該無向圖的鄰接矩陣表示爲:
咱們接下來使用程序來建立鄰接矩陣表示這個無向圖,該程序使用Python2實現:
1 import numpy as np 2 3 #返回某個頂點在頂點列表中的位置索引 4 def find_index(node, node_list): 5 for i in range(len(node_list)): 6 if node == node_list[i]: 7 return i 8 return -1 9 10 #無向圖的輸入,採用二維數組 11 data = [[1, 2], [1, 5], [2, 3], [2, 4], [3, 4], [3, 5],[4, 5]] 12 #頂點列表 13 vertex_list = [1, 2, 3, 4, 5] 14 #建立一個鄰接矩陣 15 Adj_matrix = np.zeros((5, 5), dtype=int) 16 #開始遍歷圖數據,生成鄰接矩陣。 17 for i in range(len(data)): 18 temp1 = find_index(data[i][0], vertex_list) #找到頂點在頂點列表中的索引 19 temp2 = find_index(data[i][1], vertex_list) 20 Adj_matrix[temp1][temp2] = 1 #將有邊的點出填入1 21 Adj_matrix[temp2][temp1] = 1 # 將有邊的點出填入1 22 #輸出鄰接矩陣 23 for i in range(len(Adj_matrix)): 24 for j in range(len(Adj_matrix[0])): 25 print Adj_matrix[i][j], #python2的用法,在後面加「,」表示不換行。 26 print ""
運行的結果以下:
0 1 0 0 1
1 0 1 1 0
0 1 0 1 1
0 1 1 0 1
1 0 1 1 0
下面咱們再看一個有向圖的例子,以鄰接矩陣來表示有向圖,有向圖以下圖所示:
該有向圖的鄰接矩陣表示爲:
咱們接下來使用程序來建立鄰接矩陣表示這個有向圖,該程序使用Python2實現:
1 import numpy as np 2 3 #返回某個頂點在頂點列表中的位置索引 4 def find_index(node, node_list): 5 for i in range(len(node_list)): 6 if node == node_list[i]: 7 return i 8 return -1 9 10 #有向圖的輸入,採用二維數組 11 data = [[1, 2], [2, 1], [2, 3], [2, 4], [4, 3], [4, 1]] 12 #頂點列表 13 vertex_list = [1, 2, 3, 4] 14 #建立一個鄰接矩陣 15 Adj_matrix = np.zeros((5, 5), dtype=int) 16 #開始遍歷數據,生成鄰接矩陣。 17 for i in range(len(data)): 18 temp1 = find_index(data[i][0], vertex_list) #找到頂點在頂點列表中的索引 19 temp2 = find_index(data[i][1], vertex_list) 20 Adj_matrix[temp1][temp2] = 1 #將有邊的點出填入1 21 #輸出鄰接矩陣 22 for i in range(len(Adj_matrix)): 23 for j in range(len(Adj_matrix[0])): 24 print Adj_matrix[i][j], #python2的用法,在後面加「,」表示不換行。 25 print ""
運行的結果以下:
0 1 0 0 0
1 0 1 1 0
0 0 0 0 0
1 0 1 0 0
0 0 0 0 0
2.2 鄰接表法
前面所介紹的鄰接矩陣法,優勢是憑藉着矩陣的運算又許多特別的應用。要在圖中加入新邊時,這個表示法的插入和刪除至關簡易。不過還要考慮到稀疏矩陣空間的浪費問題,另外,若是要計算全部頂點的度,其時間複雜度爲O(n^2)。
所以能夠考慮更有效的方法,就是鄰接表法(Adjacency List)。這種表示法就是將一個n行的鄰接矩陣表示成n個鏈表,這種作法和鄰接矩陣相比較節省空間,如計算全部頂點的度時,其時間複雜度爲O(n+e),缺點是:例如有新邊加入圖中或者從圖中刪除邊時,就要修改相關的連接,較爲麻煩費時。
首先,將圖的n個頂點做爲n個鏈表頭,每一個鏈表中的結點表示它們和鏈表頭結點之間有邊相連。每一個結點的數據結構以下:
1 class list_node(object): #定義一個結點類 2 def __init__(self): #構造函數 3 self.data = 0 #結點的數據域 4 self.next = None #結點的指針域
在無向圖中,由於對稱關係,如有n個頂點、m個邊,則造成n個鏈表頭,2m個結點。若在有向圖中,則有n個鏈表頭以及m個結點,所以在鄰接表中,求全部頂點的度所需的時間複雜度爲O(n+m)。
接下來,咱們看一個實際的例子,以鄰接表來表示無向圖,無向圖以下圖所示:
首先根據上圖可知,由於5個頂點使用5個鏈表頭,V1鏈表表明頂點1,與頂點1相鄰的是頂點2和頂點5,以此類推,該無向圖鄰接表表示以下:
咱們接下來使用程序來建立鄰接矩陣表示這個有向圖,該程序使用Python2實現:
1 class list_node(object): #定義一個結點類 2 def __init__(self): #構造函數 3 self.data = 0 #結點的數據域 4 self.next = None #結點的指針域 5 6 #頂點列表 7 vertex_list = [1, 2, 3, 4, 5] 8 head = [list_node] * len(vertex_list) #聲明一個結點類型的列表 9 newnode = list_node() 10 data = [[1, 2], [2, 1], [1, 5], [5, 1], [2, 3], [3, 2], [2, 4], [4, 2], [3, 4], [4, 3], [3, 5], [5, 3], [4, 5], [5, 4]] 11 print len(data) 12 print "圖的鄰接表的內容" 13 print "-----------------------------------------------------" 14 15 for i in range(len(vertex_list)): #生成頭結點,以五個頂點做爲頭結點 16 head[i].data = vertex_list[i] #分別將頂點列表的各個頂點元素存入頭結點的數據域中 17 head[i].next = None #頭結點指針域指向None 18 print "頂點 %d =>" % vertex_list[i], #打印頭結點信息 19 for j in range(len(data)): #遍歷整個傳入的圖的數據,經過它建立圖的鄰接鏈表結構 20 if data[j][0] == vertex_list[i]: #輸入數據中的起始結點等於頂點,就在終止結點加入到該頂點的鄰接鏈表中去。 21 newnode.data = data[j][1] #爲終止頂點建立一個結點信息,並將其元素值加入到數據域中 22 newnode.next = head[i].next #採用頭部插入的方式,插入該結點 23 head[i].next = newnode #這是頭部插入法 24 print "[%d] " %newnode.data, #循環打印屬於某一頭結點鄰接結點的全部數據元素。 25 print "" #表示換行
運行結果以下:
-----------------------------------------------------
頂點 1 => [2] [5]
頂點 2 => [1] [3] [4]
頂點 3 => [2] [4] [5]
頂點 4 => [2] [3] [5]
頂點 5 => [1] [3] [4]
2.3 圖的特殊表示法(利用python的基本數據結構類型)
圖是一種重要的數據結構,它能夠表明各類結構和系統,從運輸網絡到通訊網絡,從細胞核中的蛋白質相互做用到人類在線交互。圖是由頂點的有窮非空集合和頂點之間邊的集合組成,一般表示爲:G(V,E),其中,G表示一個圖,V是圖G中的頂點的集合,E是圖G中邊的集合。以下圖:
對於圖結構的實現來講,最直觀的方式之一就是使用鄰接表來表示。基本上就是針對每個節點設置一個鄰接表,而對於鄰接表的實現方式能夠不一樣,針對python的特色以及內置的數據結構,可使用列表、集合和字典來實現。
(1)鄰接集合
第一種實現鄰接表的方式是:針對每一個結點設置一個鄰居集合,在python中就是set。
1 a, b, c, d, e, f, g, h = range(8) 2 Adj_set = [ 3 {b, c, d, e, f}, 4 {c, e}, 5 {d}, 6 {e}, 7 {f}, 8 {c, g, h}, 9 {f, h}, 10 {f, g} 11 ] 12 #列表中的每一個集合是每一個結點的鄰接點集 13 14 print b in Adj_set[a] #結點b是不是結點a的鄰居結點 15 print len(Adj_set[a]) #結點a的出度
運行結果以下:
True
5
(2)鄰接列表
第二種實現鄰接表的方式是:針對每一個結點設置一個鄰居列表,在python中就是list。
1 a, b, c, d, e, f, g, h = range(8) 2 Adj_list = [ 3 [b, c, d, e, f], 4 [c, e], 5 [d], 6 [e], 7 [f], 8 [c, g, h], 9 [f, h], 10 [f, g] 11 ] 12 13 print b in Adj_list[a] #結點b是不是結點a的鄰居結點 14 print len(Adj_list[a]) #結點a的出度
運行結果以下:
True
5
(3)加權的鄰接字典
使用字典類型來代替集合或列表來表示鄰接表。在字典類型中,每一個鄰居節點都會有一個鍵和一個額外的值,用於表示與其鄰居節點(或出邊)之間的關聯性,如邊的權重。
1 a, b, c, d, e, f, g, h = range(8) 2 Adj_dict_weight = [ 3 {b: 2, c: 1, d: 3, e: 9, f: 4}, 4 {c: 4, e: 3}, 5 {d: 8}, 6 {e: 7}, 7 {f: 5}, 8 {c: 2, g: 2, h: 2}, 9 {f: 1, h: 6}, 10 {f: 9, g: 8} 11 ] 12 13 14 print b in Adj_dict_weight[a] #結點b是不是結點a的鄰居結點 15 print len(Adj_dict_weight[a]) #結點a的出度 16 print Adj_dict_weight[a][b] #邊(a,b)的權重
運行結果以下:
True
5
2
(4)鄰接集字典
以上圖的表示方法都使用了list類型,其實,也可使用字典結構dict和集合結構set的嵌套來實現。
1 Adj_set_dict = {'a':set('bcdef'), 2 'b':set('ce'), 3 'c':set('d'), 4 'd':set('e'), 5 'e':set('f'), 6 'f':set('cgh'), 7 'g':set('fh'), 8 'h':set('fg') 9 } 10 11 print Adj_set_dict["a"] #節點a的鄰居節點 12 print "b" in Adj_set_dict["a"] #節點b是不是節點a的鄰居節點
運行結果以下:
set(['c', 'b', 'e', 'd', 'f'])
True
(5)嵌套字典(最重要*****)
也可使用嵌套字典的方式來實現加權圖。
1 Nest_dict = {'a':{'b':2, 'c':1, 'd':3, 'e':9, 'f':4}, 2 'b':{'c':4, 'e':3}, 3 'c':{'d':8}, 4 'd':{'e':7}, 5 'e':{'f':5}, 6 'f':{'c':2, 'g':2, 'h':2}, 7 'g':{'f':1, 'h':6}, 8 'h':{'f':9, 'g':8} 9 }
三、圖的遍歷
圖遍歷又稱圖的遍歷,屬於數據結構中的重要內容。它指的是從圖中的任一頂點出發,對圖中的全部頂點訪問一次且只訪問一次。圖的遍歷操做和樹的遍歷操做功能類似。圖的遍歷是圖的一種基本操做,圖的許多其它操做都是創建在遍歷操做的基礎之上。
因爲圖結構自己的複雜性,因此圖的遍歷操做也較複雜,主要表如今如下四個方面:
(a)在圖結構中,沒有一個「天然」的首結點,圖中任意一個頂點均可做爲第一個被訪問的結點。
(b)在非連通圖中,從一個頂點出發,只可以訪問它所在的連通份量上的全部頂點,所以,還需考慮如何選取下一個出發點以訪問圖中其他的連通份量。
(c)在圖結構中,若是有迴路存在,那麼一個頂點被訪問以後,有可能沿迴路又回到該頂點。
(d)在圖結構中,一個頂點能夠和其它多個頂點相連,當這樣的頂點訪問事後,存在如何選取下一個要訪問的頂點的問題。
(1)深度優先遍歷(DFS)
深度優先遍歷也稱爲深度優先搜索(Depth First Search),它相似於樹的先序遍歷,具體定義以下:假設初始狀態是圖中全部頂點均未被訪問,則從某個頂點v出發,首先訪問該頂點,而後依次從它的各個未被訪問的鄰接點出發深度優先搜索遍歷圖,直至圖中全部和v有路徑相通的頂點都被訪問到。 若此時尚有其餘頂點未被訪問到,則另選一個未被訪問的頂點做起始點,重複上述過程,直至圖中全部頂點都被訪問到爲止。
1 #建立一個圖 2 Graph = {} 3 Graph['A'] = ['B', 'C', 'D'] 4 Graph['B'] = ['A', 'E'] 5 Graph['C'] = ['A', 'F'] 6 Graph['D'] = ['A', 'G', 'H'] 7 Graph['E'] = ['B', 'F'] 8 Graph['F'] = ['E', 'C'] 9 Graph['G'] = ['D', 'H', 'I'] 10 Graph['H'] = ['G', 'D'] 11 Graph['I'] = ['G'] 12 13 14 #使用堆棧來實現深度優先遍歷 15 def DFSTraverse(G, start): 16 stack = [] #初始化一個堆棧 17 visited = set() #初始化一個訪問過的節點集合 18 stack.append(start) #將起始結點入棧 19 while stack: #若是棧不爲空,進入循環 20 node = stack.pop() #棧頂元素出棧 21 if node in visited: #判斷棧頂元素是否被訪問過 22 continue #元素被訪問過,跳出循環,查看棧內的其餘元素 23 else: #棧頂元素未被訪問 24 print node, #訪問棧頂元素 25 visited.add(node) #將其加入訪問過的集合 26 for adj in G[node]: #將該元素的鄰居節點加入到棧中去 27 if adj not in visited: 28 stack.append(adj) 29 print "\n" 30 31 32 if __name__ == '__main__': 33 34 print "深度優先搜索結果:" 35 DFSTraverse(Graph, 'A')
運行結果以下:
深度優先搜索結果:
A D H G I C F E B
(2)廣度優先遍歷(BFS)
所以,使用一個隊列(Queue)輔助實現廣度優先遍歷(BFS)代碼以下(python 2.7):
1 #建立一個圖 2 Graph = {} 3 Graph['A'] = ['B', 'C', 'D'] 4 Graph['B'] = ['A', 'E'] 5 Graph['C'] = ['A', 'F'] 6 Graph['D'] = ['A', 'G', 'H'] 7 Graph['E'] = ['B', 'F'] 8 Graph['F'] = ['E', 'C'] 9 Graph['G'] = ['D', 'H', 'I'] 10 Graph['H'] = ['G', 'D'] 11 Graph['I'] = ['G'] 12 13 14 #使用隊列來實現廣度優先遍歷 15 def BFSTraverse(G, start): 16 from collections import deque 17 queue = deque() #初始化一個隊列 18 visited = set() #初始化一個存儲訪問過元素的集合 19 queue.append(start) #將起始結點加入隊列 20 while queue: #當隊列不爲空時,進入循環 21 node = queue.popleft() #將隊列的隊首元素出隊 22 if node in visited: #判斷該元素是否被訪問過,若是訪問過,跳出本次循環 23 continue 24 else: 25 print node, 26 visited.add(node) 27 for adj in G[node]: 28 if adj not in visited: 29 queue.append(adj) 30 print "\n" 31 32 33 34 35 if __name__ == '__main__': 36 37 print "廣度優先搜索結果:" 38 BFSTraverse(Graph, 'A')
運行結果以下:
廣度優先搜索結果:
A B C D E F G H I
(3)DFS和BFS算法效率比較
空間複雜度:二者的空間複雜度都是O(n),分別借用了堆棧和隊列。
時間複雜度:對於二者而言,時間複雜度只與圖的存儲結構(鄰接矩陣或鄰接表)有關,而與搜索路徑無關。鄰接矩陣:O(n^2),鄰接表:O(n + e)。
四、最小生成樹
4.1 什麼是生成樹
在圖論的數學領域中,若是連通圖 G的一個子圖是一棵包含G 的全部頂點的樹,則該子圖稱爲G的生成樹(SpanningTree)。生成樹是連通圖的包含圖中的全部頂點的極小連通子圖。圖的生成樹不唯一。從不一樣的頂點出發進行遍歷,能夠獲得不一樣的生成樹。
對於連通的帶權圖(連通網)G,其生成樹也是帶權的。生成樹T各邊的權值總和稱爲該樹的權,記做:
其中,TE表示T的邊集,w(u,v)表示邊(u,v)的權。權最小的生成樹稱爲G的最小生成樹(Minimum SpannirngTree)。最小生成樹可簡記爲MST。
求一個連通圖的最小生成樹的方法包括:Kruskal算法和Prim 算法。
4.3 最小生成樹算法
接下來咱們將以一個無向圖來展現Kruskal算法和Prim 算法,無向圖以下所示:
(1)Kruskal算法
Kruskal算法是一種用來查找最小生成樹的算法,它是基於貪心的思想獲得的。首先咱們把全部的邊按照權值先從小到大排列,接着按照順序選取每條邊,若是這條邊的兩個端點不屬於同一集合,那麼就將它們合併,直到全部的點都屬於同一個集合爲止。至於怎麼合併到一個集合,那麼這裏咱們就能夠用到一個工具——-並查集。換而言之,Kruskal算法就是基於並查集的貪心算法。
Kruskal算法每次要從都要從剩餘的邊中選取一個最小的邊。一般咱們要先對邊按權值從小到大排序,這一步的時間複雜度爲爲O(|Elog|E|)。Kruskal算法的實現一般使用並查集,來快速判斷兩個頂點是否屬於同一個集合。最壞的狀況可能要枚舉完全部的邊,此時要循環|E|次,因此這一步的時間複雜度爲O(|E|α(V)),其中α爲Ackermann函數,其增加很是慢,咱們能夠視爲常數。因此Kruskal算法的時間複雜度爲O(|Elog|E|),其中E和V分別是圖的邊集和點集。
所以,使用Kruskal算法查找最小生成樹的代碼以下(python 2.7):
1 Graph = {'A': {'B': 6, 'E': 10, 'F': 12}, 2 'B': {'A': 6, 'C': 3, 'D': 5, 'F': 8}, 3 'C': {'B': 3, 'D': 7}, 4 'D': {'B': 5, 'C': 7, 'E': 9, 'F': 11}, 5 'E': {'A': 10, 'D': 9, 'F': 16}, 6 'F': {'A': 12, 'B': 8, 'D': 11, 'E': 16}, 7 } 8 9 def Kruskal(G): 10 def f1(x): 11 return x[2] 12 record_node = set() #記錄添加的邊節點 13 mintree = [] #最小生成樹的全部的邊 14 cost = [] 15 edges = [] #得到圖的全部的邊,並對它排序 16 for key1 in G.keys(): 17 for key2 in G[key1].keys(): 18 edges.append([key1, key2, G[key1][key2]]) 19 edges.sort(key=f1, reverse=True) #對全部的邊的成本進行從大到小排序 20 21 while edges: 22 edge = edges.pop() #選取最短的邊 23 if (edge[0] in record_node) and (edge[1] in record_node): #若是這兩個頂點同時在集合中,表示會造成環路,不能加入這條邊 24 continue 25 else: 26 record_node.add(edge[0]) 27 record_node.add(edge[1]) 28 cost.append(edge.pop()) 29 mintree.append(edge) 30 print "克魯斯卡爾Kruskal算法最小生成樹:" 31 print "各個邊的權值: ", cost 32 print "最小生成樹的成本: ", sum(cost) 33 print "最小生成樹的邊: ", mintree 34 return cost, mintree 35 36 if __name__ == '__main__': 37 cost,mintree = Kruskal(Graph) 38 print "\n"
運行結果以下:
克魯斯卡爾Kruskal算法最小生成樹:
各個邊的權值: [3, 5, 6, 8, 9]
最小生成樹的成本: 31
最小生成樹的邊: [['B', 'C'], ['D', 'B'], ['B', 'A'], ['F', 'B'], ['D', 'E']]
(2)Prim算法
普里姆算法(Prim算法)是圖論中的一種算法,可在加權連通圖裏搜索最小生成樹。意即由此算法搜索到的邊子集所構成的樹中,不但包括了連通圖裏的全部頂點,且其全部邊的權值之和亦爲最小。
對於任意圖,假設包含n個頂點,m條邊。Prim算法是從頂點出發的,其算法時間複雜度與頂點數目有關係。(注意:prim算法適合稠密圖,其時間複雜度爲O(n^2),其時間複雜度與邊得數目無關,而Kruskal算法的時間複雜度爲O(ElogE)跟邊的數目有關,適合稀疏圖。)
Prim算法是一種構造性算法。假設G=(V,E)是一個具備n個頂點的帶權連通無向圖,T=(U,TE)是G的最小生成樹,其中U是T的頂點集,TE是T的邊集,則由G構造從起始頂點v出發的最小生成樹T的步驟以下:
(a)初始化U={v},以v到其餘頂點的全部邊爲候選邊;
(b)重複如下步驟(n-1)次,使得其餘(n-1)個頂點被加入到U中:
(1)從侯選邊中挑選權值最小的邊加入TE,設該邊在V-U中的頂點是k,將k加入U中;(加入後不能造成環)
(2)考察當前V-U中全部頂點j,修改侯選邊,若邊(k,j)的權值小於原來和頂點j關聯的侯選邊,則用邊(k,j)取代後者做爲侯選邊。(加入後不能造成環)
所以,使用Prim算法查找最小生成樹的代碼以下(python 2.7):
1 Graph = {'A': {'B': 6, 'E': 10, 'F': 12}, 2 'B': {'A': 6, 'C': 3, 'D': 5, 'F': 8}, 3 'C': {'B': 3, 'D': 7}, 4 'D': {'B': 5, 'C': 7, 'E': 9, 'F': 11}, 5 'E': {'A': 10, 'D': 9, 'F': 16}, 6 'F': {'A': 12, 'B': 8, 'D': 11, 'E': 16}, 7 } 8 9 def Prim(G): 10 U = set(G.keys()) #圖G的頂點集合U,它包含了該圖的全部頂點 11 V = set(G.keys()[0]) #將起始頂點加入集合V 12 min_tree = [] #存儲要返回的最小生成樹的全部的邊 13 cost = [] #記錄最小生成樹各邊的權重的值 14 while U.difference(V): #當集合U和V不想等時,進入循環 15 min_value = float("inf") #初始化一個最小值 16 node1 = None #用於記錄加入邊的第一個節點 17 node2 = None #用於記錄加入邊的第二個節點 18 for v in V: #遍歷訪問過的節點 19 for u in U.difference(V): #遍歷未訪問過的節點 20 if u in G[v]: #若是兩個節點之間存在相連的邊 21 if G[v][u] < min_value: #判斷該值是不是全部訪問過節點存在的相鄰邊中的最小值 22 min_value = G[v][u] #更新邊權重的最小值 23 node1 = v #記錄邊權重最小值的第一個節點 24 node2 = u #記錄邊權重最小值的第二個節點 25 V.add(node2) #將第二個節點加入到訪問過的節點集合V,由於第二個節點來自於未訪問過的節點集合 26 min_tree.append([node1, node2]) #將包括兩個節點的邊加入到最小生成樹邊列表 27 cost.append(min_value) #將邊的權重加入到成本列表中 28 print "普利姆Prim算法最小生成樹:" 29 print "各個邊的權值: ", cost 30 print "最小生成樹的成本: ", sum(cost) 31 print "最小生成樹的邊: ", min_tree 32 return cost, min_tree 33 34 if __name__ == '__main__': 35 36 Prim(Graph)
運行結果以下:
普利姆Prim算法最小生成樹:
各個邊的權值: [6, 3, 5, 8, 9]
最小生成樹的成本: 31
最小生成樹的邊: [['A', 'B'], ['B', 'C'], ['B', 'D'], ['B', 'F'], ['D', 'E']]
(3)兩種算法比較
五、最短路徑問題