談到算法,圖的操作是避免不了。
而我們一般談到圖時,又必定會談到圖的遍歷。
圖的遍歷通常有 2 種,深度優先(DFS) 和廣度優先(BFS)。
深度優先可以閱讀我這篇博文:【小算法】圖的遍歷之深度優先(DFS)
本篇博文講解廣度優先(BFS)。
圖的表示
圖有兩種表示方式
1. 臨接矩陣
其實就是一個權重矩陣,用 1 代表兩個結點有連接,0 表示沒有連接,這樣的表示方式通俗易懂,特別適合稠密圖,也就是大多數結點是亮亮連接的情況。
2. 臨接表
用一個數組儲存所有的頂點的信息,每個頂點又用一個鏈表或者是數組存放與它相臨的結點的信息。
這樣的表示方式特別適合稀疏圖,也就是比較少的結點之間相互有連接。
本文示例代碼用 Python 表示,爲了簡便,用臨接表這種形式表示
BFS 算法思路
其實 BFS 的思路非常簡單。
如果你哪天錢包忘記在哪裏了,以 BFS 的思路就是有層次地搜索。
先每個房間快速地瞄一眼,如果沒有發現的話,那麼就在每個房間的牀上、桌子上快速瞄一眼。
如果還是不行的話,再在每個傢俱的每個櫃子裏快速瞄一眼。
然後,按照這樣一層一層進行下去。
DFS 圖例
上面是一張圖,如果要遍歷圖中所有的結點,又不重複。
在實際編碼中,如果要用 BFS 的方式去遍歷一個圖的話,通常我們會用一個隊列來動態保存陸續訪問的結點。
我們首先選擇 A.
所以 A 先入隊列。
A 有 2 個臨接結點 B 和 C,所以 B 和 C 依次入隊列。
並且將 A 從隊列中彈出。
A 結點出隊後,現在隊列首個元素是 B 結點,B 結點有 4 個臨接點 A、C、D、F。
因爲 A 和 C 已經入隊列過一次,所以不能再入,因此將 D、F 入列。
最後,將 B 結點及時彈出來。
B 結點彈出來後,C 結點就是隊列的首元素,它有 A、B、E 3 個臨結點,但是 A 和 B 已經入隊列訪問過,所以它只需要將 E 入列就好了。
然後,C 將自己彈出來。
隊列首元素就變成了 D.
由於 D 結點所有的臨接點都已經入隊列過了,代表訪問過了,所以它彈出自己就好。
後面的 F 和 E 也是這樣的邏輯,就不贅述了。
最後,隊列所有的元素彈出後,也沒有新的元素可以再入列的時候,也就說明完整的 BFS 過程結束了。
巧妙地利用隊列先進先出(FIFO)的特性用來保存遍歷的結點痕跡,最終實現了無重複也無遺漏的圖的遍歷,這種算法思想非常的贊。
下面用代碼示例驗證一下,python 版本是 3.6
Python 代碼
bfs.py
import queue
# G 是臨接表的方式表達圖形
G={}
G[0] = {1,2}
G[1] = {2,3,5}
G[2] = {0,1,3}
G[3] = {2,4,5}
G[4] = {1,3,5}
G[5] = {1,3,4}
# 記錄訪問過的結點
visited = [False,False,False,False,False,False]
# 結點的別名
names=['A','B','C','D','E','F']
vertex_queue = queue.Queue()
def bfs(root):
vertex_queue.put(root)
visited[root] = True
while not vertex_queue.empty():
v = vertex_queue.get()
print("----->",names[v])
for i,value in enumerate(G[v]):
# 已經入過 queue 的結點就不用再入
if not visited[value]:
visited[value] = True
vertex_queue.put(value)
if __name__ == "__main__":
# 打印圖
print("Graphy:",G)
#dfs(0)
bfs(0)
最終結果如下:
Graphy: {0: {1, 2}, 1: {2, 3, 5}, 2: {0, 1, 3}, 3: {2, 4, 5}, 4: {1, 3, 5}, 5: {1, 3, 4}}
---> A
---> B
---> C
---> D
---> F
---> E
可以看到,代碼可以正常執行,大家親手實踐一下吧。