算法詳解之廣度優先搜索算法

算法簡介

廣度優先搜索算法(Breadth First Search,BFS),又稱爲寬度優先搜索, 是用於的一種簡單遍歷算法。它並不考慮結果的可能位置,徹底的搜索整張圖,直到找到結果爲止,是一種盲目搜索算法。

BFS用於解決兩個問題:

  1. 判斷從A點到B點是否有可達路徑。
  2. 從A點出發到B點的最短路徑(這裏的最短路徑是指經過的步驟最少)。

時間複雜度

廣度優先搜索算法會沿着每條Edge)經過每個頂點Vertice)逐一掃描,掃描每條邊至少需要O(E)O(E),而爲了保證掃描節點的順序性,需要使用隊列逐個添加各個頂點,因此掃描頂點至少也需要O(V)O(V),因此其時間複雜度通常爲:
O(V+E) O(V + E)

圖的分類

  • 無向圖: 頂點之間的邊沒有方向(即邊沒有箭頭指向),表示兩個頂點互爲鄰居節點,如下圖所示:
    無向圖
  • 有向圖: 頂點之間的邊具有方向(即從邊有箭頭指向),表示從一個頂點到另一個頂點的邊是有方向的,如果兩個頂點之間互相指向,則等價於無向圖,如下圖所示:
    有向圖1
    有向圖2
  • 連通圖: 如果各個頂點之間均有可達路徑,則可以稱爲連通圖,如下圖所示:
    連通圖

案例

下面我們使用無向圖來演示廣度優先算法的原理,如下圖所示:
BFS1
如上圖所示,現在我們要找出從StartEnd節點的最短路徑,此時就可以使用廣度優先搜索算法,具體算法原理步驟如下:

  1. 假設存在一個空的搜索隊列Queue,首先將節點Start所有鄰居節點添加到隊列中;
  2. 每次從隊列中取出一個節點,並判斷該節點是否是需要查找的目標節點,若不是,則將該節點的所有鄰居節點也添加到隊列中(注意隊列的先進先出特性),並將該節點從隊列中移除,同時將該節點加入到已處理節點集合(processed)中(防止循環處理節點導致死循環);
  3. 重複步驟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);
    }

思考

我們這裏在求解最短路徑時,是默認假設各個頂點到達路徑上開銷是相同的,我們使用廣度優先搜索算法得到的 “最短路徑” 只是起點到終點所要經過的最少步驟,我們忽略了實際每條邊所需要的真實開銷。在不考慮其他任何因素,只看行動步驟的話,確實滿足了我們的要求,但是這個結果真的就是 “最短路徑” 嗎?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章