算法简介
广度优先搜索算法(Breadth First Search,BFS),又称为宽度优先搜索, 是用于图的一种简单遍历算法。它并不考虑结果的可能位置,彻底的搜索整张图,直到找到结果为止,是一种盲目搜索算法。
BFS用于解决两个问题:
- 判断从A点到B点是否有可达路径。
- 从A点出发到B点的最短路径(这里的最短路径是指经过的步骤最少)。
时间复杂度
广度优先搜索算法会沿着每条边(Edge)经过每个顶点(Vertice)逐一扫描,扫描每条边至少需要
O
(
E
)
O(E)
O(E),而为了保证扫描节点的顺序性,需要使用队列逐个添加各个顶点,因此扫描顶点至少也需要
O
(
V
)
O(V)
O(V),因此其时间复杂度通常为:
O
(
V
+
E
)
O(V + E)
O(V+E)
图的分类
- 无向图: 顶点之间的边没有方向(即边没有箭头指向),表示两个顶点互为邻居节点,如下图所示:
- 有向图: 顶点之间的边具有方向(即从边有箭头指向),表示从一个顶点到另一个顶点的边是有方向的,如果两个顶点之间互相指向,则等价于无向图,如下图所示:
- 连通图: 如果各个顶点之间均有可达路径,则可以称为连通图,如下图所示:
案例
下面我们使用无向图来演示广度优先算法的原理,如下图所示:
如上图所示,现在我们要找出从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);
}
思考
我们这里在求解最短路径时,是默认假设各个顶点到达路径上开销是相同的,我们使用广度优先搜索算法得到的 “最短路径” 只是起点到终点所要经过的最少步骤,我们忽略了实际每条边所需要的真实开销。在不考虑其他任何因素,只看行动步骤的话,确实满足了我们的要求,但是这个结果真的就是 “最短路径” 吗?