圖的概念
圖表示的是多點之間的連接關系,由節點和邊組成。類型分為有向圖,無向圖,加權圖等,任何問題只要能抽象為圖,那么就可以應用相應的圖算法。
用字典來表示圖
這里我們以有向圖舉例,有向圖的鄰居節點是要順着箭頭方向,逆箭頭方向的節點不算作鄰居節點。
在python中,我們使用字典來表示圖,我們將圖相鄰節點之間的連接轉換為字典鍵值之間的映射關系。比如上圖中的1的相鄰節點為2和3,即可表示如下:
graph={}
graph[1] = [2,3]
按照這種方式,上圖可以完整表示為:
graph={}
graph[1] = [3,2] # 這里為了演示,調換一下位置
graph[2] = [5]
graph[3] = [4,7]
graph[4] = [6]
graph[5] = [6]
graph[6] = [8]
graph[7] = [8]
graph[8] = []
如此我們將所有節點和其相鄰節點之間的連接關系全部描述一遍就得到了圖的字典表示形式。節點8由於沒有相鄰節點,我們將其置為空列表。
廣度優先搜索
廣度優先搜索和深度優先搜索是圖遍歷的兩種算法,廣度和深度的區別在於對節點的遍歷順序不同。廣度優先算法的遍歷順序是由近及遠,先看到的節點先遍歷。
接下來使用python實現廣度優先搜索並找到最短路徑:
from collections import deque
from collections import namedtuple
def bfs(start_node, end_node, graph): # 開始節點 目標節點 圖字典
node = namedtuple('node', 'name, from_node') # 使用namedtuple定義節點,用於存儲前置節點
search_queue = deque() # 使用雙端隊列,這里當作隊列使用,根據先進先出獲取下一個遍歷的節點
name_search = deque() # 存儲隊列中已有的節點名稱
visited = {} # 存儲已經訪問過的節點
search_queue.append(node(start_node, None)) # 填入初始節點,從隊列后面加入
name_search.append(start_node) # 填入初始節點名稱
path = [] # 用戶回溯路徑
path_len = 0 # 路徑長度
print('開始搜索...')
while search_queue: # 只要搜索隊列中有數據就一直遍歷下去
print('待遍歷節點: ', name_search)
current_node = search_queue.popleft() # 從隊列前邊獲取節點,即先進先出,這是BFS的核心
name_search.popleft() # 將名稱也相應彈出
if current_node.name not in visited: # 當前節點是否被訪問過
print('當前節點: ', current_node.name, end=' | ')
if current_node.name == end_node: # 退出條件,找到了目標節點,接下來執行路徑回溯和長度計算
pre_node = current_node # 路徑回溯的關鍵在於每個節點中存儲的前置節點
while True: # 開啟循環直到找到開始節點
if pre_node.name == start_node: # 退出條件:前置節點為開始節點
path.append(start_node) # 退出前將開始節點也加入路徑,保證路徑的完整性
break
else:
path.append(pre_node.name) # 不斷將前置節點名稱加入路徑
pre_node = visited[pre_node.from_node] # 取出前置節點的前置節點,依次類推
path_len = len(path) - 1 # 獲得完整路徑后,長度即為節點個數-1
break
else:
visited[current_node.name] = current_node # 如果沒有找到目標節點,將節點設為已訪問,並將相鄰節點加入搜索隊列,繼續找下去
for node_name in graph[current_node.name]: # 遍歷相鄰節點,判斷相鄰節點是否已經在搜索隊列
if node_name not in name_search: # 如果相鄰節點不在搜索隊列則進行添加
search_queue.append(node(node_name, current_node.name))
name_search.append(node_name)
print('搜索完畢,最短路徑為:', path[::-1], "長度為:", path_len) # 打印搜索結果
if __name__ == "__main__":
graph = dict() # 使用字典表示有向圖
graph[1] = [3, 2]
graph[2] = [5]
graph[3] = [4, 7]
graph[4] = [6]
graph[5] = [6]
graph[6] = [8]
graph[7] = [8]
graph[8] = []
bfs(1, 8, graph) # 執行搜索
搜索結果
開始搜索...
待遍歷節點: deque([1])
當前節點: 1 | 待遍歷節點: deque([3, 2])
當前節點: 3 | 待遍歷節點: deque([2, 4, 7])
當前節點: 2 | 待遍歷節點: deque([4, 7, 5])
當前節點: 4 | 待遍歷節點: deque([7, 5, 6])
當前節點: 7 | 待遍歷節點: deque([5, 6, 8])
當前節點: 5 | 待遍歷節點: deque([6, 8])
當前節點: 6 | 待遍歷節點: deque([8])
當前節點: 8 | 搜索完畢,最短路徑為: [1, 3, 7, 8] 長度為: 3
廣度優先搜索的適用場景:只適用於深度不深且權值相同的圖,搜索的結果為最短路徑或者最小權值和。
深度優先搜索
深度優先搜索的遍歷順序為一條路徑走到底然后回溯再走下一條路徑,這種遍歷方法很省內存但是不能一次性給出最短路徑或者最優解。
用python實現深度優先算法只需要在廣度的基礎上將搜索隊列改為搜索棧即可:
from collections import deque
from collections import namedtuple
def bfs(start_node, end_node, graph):
node = namedtuple('node', 'name, from_node')
search_stack = deque() # 這里當作棧使用
name_search = deque()
visited = {}
search_stack.append(node(start_node, None))
name_search.append(start_node)
path = []
path_len = 0
print('開始搜索...')
while search_stack:
print('待遍歷節點: ', name_search)
current_node = search_stack.pop() # 使用棧模式,即后進先出,這是DFS的核心
name_search.pop()
if current_node.name not in visited:
print('當前節點: ', current_node.name, end=' | ')
if current_node.name == end_node:
pre_node = current_node
while True:
if pre_node.name == start_node:
path.append(start_node)
break
else:
path.append(pre_node.name)
pre_node = visited[pre_node.from_node]
path_len = len(path) - 1
break
else:
visited[current_node.name] = current_node
for node_name in graph[current_node.name]:
if node_name not in name_search:
search_stack.append(node(node_name, current_node.name))
name_search.append(node_name)
print('搜索完畢,路徑為:', path[::-1], "長度為:", path_len) # 這里不再是最短路徑,深度優先搜索無法一次給出最短路徑
if __name__ == "__main__":
graph = dict()
graph[1] = [3, 2]
graph[2] = [5]
graph[3] = [4, 7]
graph[4] = [6]
graph[5] = [6]
graph[6] = [8]
graph[7] = [8]
graph[8] = []
bfs(1, 8, graph)
搜索結果
開始搜索...
待遍歷節點: deque([1])
當前節點: 1 | 待遍歷節點: deque([3, 2])
當前節點: 2 | 待遍歷節點: deque([3, 5])
當前節點: 5 | 待遍歷節點: deque([3, 6])
當前節點: 6 | 待遍歷節點: deque([3, 8])
當前節點: 8 | 搜索完畢,路徑為: [1, 2, 5, 6, 8] 長度為: 4
python的deque根據pop還是popleft可以當成棧或隊列使用,DFS的能夠很快給出解,但不一定是最優解。
深度優先搜索的適用場景: 針對深度很深或者深度不確定的圖或者權值不相同的圖可以適用DFS,優勢在於節省資源,但想要得到最優解需要完整遍歷后比對所有路徑選取最優解。