算法簡介
廣度優先搜索算法(Breadth First Search,BFS),又稱爲寬度優先搜索, 是用於圖的一種簡單遍歷算法。它並不考慮結果的可能位置,徹底的搜索整張圖,直到找到結果爲止,是一種盲目搜索算法。
BFS用於解決兩個問題:
- 判斷從A點到B點是否有可達路徑。
- 從A點出發到B點的最短路徑(這裏的最短路徑是指經過的步驟最少)。
時間複雜度
廣度優先搜索算法會沿着每條邊(Edge)經過每個頂點(Vertice)逐一掃描,掃描每條邊至少需要,而爲了保證掃描節點的順序性,需要使用隊列逐個添加各個頂點,因此掃描頂點至少也需要,因此其時間複雜度通常爲:
圖的分類
- 無向圖: 頂點之間的邊沒有方向(即邊沒有箭頭指向),表示兩個頂點互爲鄰居節點,如下圖所示:
- 有向圖: 頂點之間的邊具有方向(即從邊有箭頭指向),表示從一個頂點到另一個頂點的邊是有方向的,如果兩個頂點之間互相指向,則等價於無向圖,如下圖所示:
- 連通圖: 如果各個頂點之間均有可達路徑,則可以稱爲連通圖,如下圖所示:
案例
下面我們使用無向圖來演示廣度優先算法的原理,如下圖所示:
如上圖所示,現在我們要找出從Start到End節點的最短路徑,此時就可以使用廣度優先搜索算法,具體算法原理步驟如下:
- 假設存在一個空的搜索隊列Queue,首先將節點Start的所有鄰居節點添加到隊列中;
- 每次從隊列中取出一個節點,並判斷該節點是否是需要查找的目標節點,若不是,則將該節點的所有鄰居節點也添加到隊列中(注意隊列的先進先出特性),並將該節點從隊列中移除,同時將該節點加入到已處理節點集合(processed)中(防止循環處理節點導致死循環);
- 重複步驟2,直到找到目標節點或隊列爲空時結束算法。
代碼實現
Python實現
from collections import deque
def bfs(start,end,graph):
search_queue = deque() # 使用deque來表示掃描隊列,將待掃描節點逐次添加到隊列中
search_queue += graph[start] # 將起點的所有鄰居節點加入到隊列中
processed = [start] # 已處理節點列表,起點默認爲已掃描節點
path = [] # 最優路徑節點列表
# 開始遍歷隊列,直到隊列爲空
while search_queue:
current_node = search_queue.popleft() # 將第一個元素出隊作爲當前掃描節點
print('當前節點: ', current_node)
# 保證當前節點是未掃描節點
if current_node not in processed:
# 判斷當前節點是否爲目標節點
if current_node == end:
processed.append(current_node) # 將目標節點也加入到已處理列表
path.append(current_node) # 將目標節點也加入到最優路徑節點列表中
# 掃描已處理節點列表得到最優路徑
while current_node != start:
for pre_node in processed:
# 判斷當前節點是否存在前一節點的鄰居節點列表中
if current_node in graph[pre_node]:
# 若當前節點存在於前一節點的鄰居節點列表中,則表明前一節點爲當前節點的父節點
current_node = pre_node
path.append(current_node)
break
break
else:
# 如果當前節點不是目標節點,則將當前節點的鄰居節點也加入到隊列中
neighbors = graph[current_node] # 當前節點的鄰居節點
print('當前節點 %s 的鄰居節點 %s' % (current_node, neighbors))
search_queue += neighbors # 將鄰居節點加入到隊列中
# 將當前節點加入到已處理節點列表中
processed.append(current_node)
if path:
print('最短路徑: %s' % ' -> '.join(path[::-1]))
print('最少步數: %d' % (len(path) - 1))
else:
print('節點 %s 到 %s 沒有可達路徑' % (start, end))
if __name__ == '__main__':
# 使用散列表+列表的方式表示圖結構
graph = dict()
graph['Start'] = ['A', 'B']
graph['A'] = ['C', 'start']
graph['B'] = ['D', 'E', 'start']
graph['C'] = ['A', 'D', 'End']
graph['D'] = ['B', 'C']
graph['E'] = ['B', 'F']
graph['F'] = ['E', 'End']
graph['End'] = ['C', 'F']
bfs('Start','End',graph)
Java實現
public static void bfs(String start, String end, Map<String, List<String>> graph) {
//節點掃描隊列,保存待掃描節點
Queue<String> searchQueue = new LinkedList<>();
//已處理節點列表,保存已掃描過的節點
List<String> processed = new ArrayList<>();
//起點默認爲已處理節點
processed.add(start);
//最優路徑節點列表
List<String> path = new ArrayList<>();
//將起點的所有鄰居節點加入到掃描隊列
searchQueue.addAll(graph.get(start));
//開始掃描隊列,直到找到目標節點或隊列爲空爲止
while (searchQueue.size() > 0) {
//從隊列中取出一個元素
String current_node = searchQueue.poll();
System.out.printf("當前節點爲: %s\n",current_node);
if (!processed.contains(current_node)) {
//如果當前節點爲目標節點
if (current_node.equalsIgnoreCase(end)) {
//將目標節點也加入到已處理列表中
processed.add(current_node);
//將目標節點加入到最優路徑節點列表中
path.add(current_node);
//遍歷已處理節點列表,得出最優路徑節點列表
while (!current_node.equalsIgnoreCase(start)) {
for (String pre_node : processed) {
//判斷當前節點是否在前一節點的鄰居節點中
if (graph.get(pre_node).contains(current_node)) {
current_node = pre_node;
path.add(current_node);
break;
}
}
}
break;
} else {
//如果當前節點不是目標節點,則將其所有鄰居節點加入到隊列中
List<String> neighbors = graph.get(current_node);
System.out.printf("加入節點 %s 的鄰居節點 %s\n",current_node,neighbors);
searchQueue.addAll(neighbors);
//將當前節點加入到已處理列表中
processed.add(current_node);
}
}
}
if (path.size() > 0) {
Collections.reverse(path);
String pathStr = path.stream().collect(Collectors.joining(" -> "));
System.out.printf("最短路徑爲: %s\n",pathStr);
System.out.printf("最少步數: %d\n",path.size() - 1);
} else {
System.out.printf("節點 %s 到 %s沒有可達路徑",start,end);
}
}
public static void main(String[] args) {
Map<String,List<String>> graph = new HashMap<>();
graph.put("Start",Arrays.asList("A","B"));
graph.put("A",Arrays.asList("C","Start"));
graph.put("B",Arrays.asList("D","E","Start"));
graph.put("C",Arrays.asList("A","D","End"));
graph.put("D",Arrays.asList("B","C"));
graph.put("E",Arrays.asList("B","F"));
graph.put("F",Arrays.asList("E","End"));
graph.put("End",Arrays.asList("C","F"));
bfs("Start","End",graph);
}
思考
我們這裏在求解最短路徑時,是默認假設各個頂點到達路徑上開銷是相同的,我們使用廣度優先搜索算法得到的 “最短路徑” 只是起點到終點所要經過的最少步驟,我們忽略了實際每條邊所需要的真實開銷。在不考慮其他任何因素,只看行動步驟的話,確實滿足了我們的要求,但是這個結果真的就是 “最短路徑” 嗎?