圖,特別是圖的ADT(抽象數據類型)在計算機科學和數學領域是應用非常廣泛的。
圖基本認識
圖模擬一組連接,假如你在A點,你到B點的路徑,可以使用圖來代表。
圖由圖由頂點(vertex,node)和邊(edge)組成。一個頂點(vertex)可以與多個頂點連接,通過一條線,這條線稱爲邊(edge)。
圖用於模擬一組數據是如何連接的。
在Python中使用字典來實現這種連接關係。在之前的文章,我已經介紹了Python字典的實現原理。
廣度優先搜索(breadth-first search, BFS)
首先,BFS可以解決兩種問題
- 從節點A出發,能不能到節點X?
- 從節點A出發,到節點X的最短路徑是哪條?
如何理解廣度:
假如現在你需要在你的朋友的關係網中,找到一個賣芒果的,應該如何做?下圖是你的朋友關係網的情況。
廣度優先,就是, 在你的關係網中,一度關係勝過二度關係。廣度優先,就是先在一度關係中找到,再往外層的度找。
現在我們用BFS來解決第一個問題。
這裏參考了《算法圖解》的例子。
先把上圖寫爲對應的Python代碼:
graph = dict()
graph['your'] = ['alice', 'boob', 'claire']
graph['boob'] = ['anuj', 'peggy']
graph['alice'] = ['peggy']
graph['claire'] = ['thom', 'jonny']
graph['anuj'] = []
graph['peggy'] = []
graph['thom'] = []
graph['jonny'] = []
我們使用隊列(FIFO)的思想,將關係網的人加入到隊列,然後彈出一個元素,並且把該元素的關係網的朋友也加入到隊列。下面是代碼實現:
def person_is_seller(name):
return name[-1] == 'm'
def search(graph, name):
search_queue = deque()
search_queue += graph[name]
searched = []
while search_queue:
person = search_queue.popleft()
if person not in search_queue:
if person_is_seller(person):
print(searched)
print(person + ' is a mango seller')
return True
else:
search_queue += graph[person]
searched.append(person)
return False
這樣的代碼只是告訴了我們,在關係網中能夠找到那個人(以m
字母結尾),並且可以得到那個人的信息。
上面的代碼還是比較簡單的,現在我們來解決第二個問題,最短路徑是哪個?也就是,如何把走過的路徑保存起來?
def bfs2(graph, start):
queue = [(start, [start])] # ['your', ['your']]
while queue:
(vertex, path) = queue.pop(0)
for next_friend in graph[vertex]:
if person_is_seller(next_friend):
yield path + [next_friend]
else:
queue.append((next_friend, path + [next_friend]))
我們使用列表模擬隊列也是一樣的, 你也可以使用列表來代替yield。這裏其實還有一個問題,就是可能會重複計算。因爲並沒有像之前那樣,把查找過的路徑進行判斷。由於我們的graph的數據只是單指向的,所以也並不會有什麼問題,並且,這樣更容易理解一些。下面解釋了上面的代碼做了什麼:
- 在queue裏面存儲每一個人(在圖論中稱爲頂點,vertex),以及它的路徑,用一個元組包裹。
- 循環這個queue,每次只彈出最先進去的元素,得到一個元組
(頂點,路徑)
- 根據元組的頂點得到graph的數據(朋友的朋友),遍歷這些朋友
- 如果不是要找的芒果商,那麼把該朋友也添加到隊列裏面去,同時把路徑也保存。
現在,在if判斷加===
,在else加打印queue
,再給jonny
添加一位朋友
graph['jonny'] = ['abcem']
[('alice', ['your', 'alice'])]
[('alice', ['your', 'alice']), ('boob', ['your', 'boob'])]
[('alice', ['your', 'alice']), ('boob', ['your', 'boob']), ('claire', ['your', 'claire'])]
[('boob', ['your', 'boob']), ('claire', ['your', 'claire']), ('peggy', ['your', 'alice', 'peggy'])]
[('claire', ['your', 'claire']), ('peggy', ['your', 'alice', 'peggy']), ('anuj', ['your', 'boob', 'anuj'])]
[('claire', ['your', 'claire']), ('peggy', ['your', 'alice', 'peggy']), ('anuj', ['your', 'boob', 'anuj']), ('peggy', ['your', 'boob', 'peggy'])]
===
[('peggy', ['your', 'alice', 'peggy']), ('anuj', ['your', 'boob', 'anuj']), ('peggy', ['your', 'boob', 'peggy']), ('jonny', ['your', 'claire', 'jonny'])]
===
[['your', 'claire', 'thom'], ['your', 'claire', 'jonny', 'abcem']]
可以看到,最近一度的朋友總是會被先探測到。這就是廣度優先查找。
到此,基本的廣度優先查找算法已經實現。
深度優先查找
深度優先查找,只要把隊列變成棧即可。
def dfs(graph, start):
queue = [(start, [start])] # 1: ['A', ['A']]
paths = []
while queue:
(vertex, path) = queue.pop()
for next_friend in graph[vertex]:
if person_is_seller(next_friend):
print('===')
paths.append(path + [next_friend])
else:
queue.append((next_friend, path + [next_friend]))
print(queue)
return paths
- 第一次, 會把your的三個朋友壓入棧
- 彈出後入棧的朋友,找到他的朋友,再添加入棧,以此類推,每次出棧都會是最新添加的
由於每次出棧的都是新的數據,所以會深入一條一條朋友網的線找下去。這就是深度優先。