算法详解之广度优先搜索算法

算法简介

广度优先搜索算法(Breadth First Search,BFS),又称为宽度优先搜索, 是用于的一种简单遍历算法。它并不考虑结果的可能位置,彻底的搜索整张图,直到找到结果为止,是一种盲目搜索算法。

BFS用于解决两个问题:

  1. 判断从A点到B点是否有可达路径。
  2. 从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)

图的分类

  • 无向图: 顶点之间的边没有方向(即边没有箭头指向),表示两个顶点互为邻居节点,如下图所示:
    无向图
  • 有向图: 顶点之间的边具有方向(即从边有箭头指向),表示从一个顶点到另一个顶点的边是有方向的,如果两个顶点之间互相指向,则等价于无向图,如下图所示:
    算法详解之广度优先搜索算法_第1张图片
    有向图2
  • 连通图: 如果各个顶点之间均有可达路径,则可以称为连通图,如下图所示:
    算法详解之广度优先搜索算法_第2张图片

案例

下面我们使用无向图来演示广度优先算法的原理,如下图所示:
算法详解之广度优先搜索算法_第3张图片
如上图所示,现在我们要找出从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);
    }

思考

我们这里在求解最短路径时,是默认假设各个顶点到达路径上开销是相同的,我们使用广度优先搜索算法得到的 “最短路径” 只是起点到终点所要经过的最少步骤,我们忽略了实际每条边所需要的真实开销。在不考虑其他任何因素,只看行动步骤的话,确实满足了我们的要求,但是这个结果真的就是 “最短路径” 吗?

你可能感兴趣的:(算法专题,#,图算法)