广度优先搜索算法(Breadth First Search,BFS),又称为宽度优先搜索, 是用于图的一种简单遍历算法。它并不考虑结果的可能位置,彻底的搜索整张图,直到找到结果为止,是一种盲目搜索算法。
BFS用于解决两个问题:
广度优先搜索算法会沿着每条边(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节点的最短路径,此时就可以使用广度优先搜索算法,具体算法原理步骤如下:
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);
}
我们这里在求解最短路径时,是默认假设各个顶点到达路径上开销是相同的,我们使用广度优先搜索算法得到的 “最短路径” 只是起点到终点所要经过的最少步骤,我们忽略了实际每条边所需要的真实开销。在不考虑其他任何因素,只看行动步骤的话,确实满足了我们的要求,但是这个结果真的就是 “最短路径” 吗?