广度优先遍历:宽度优先遍历(Breadth-First Search, BFS), 图论和树论中基本的查找搜索算法, 是广大图算法的基础.。
前置知识和介绍
数据结构
: 队列, 双端队列。 二叉树:经典bfs,按层bfs(即树的层序遍历)。
编程语言: Java
广度优先遍历的细节和可玩性很高。 广度优先遍历英文名缩写是bfs。 所以,大佬口中的bfs☞广度优先遍历。 二叉树,多叉树中的层序遍历也是广度优先遍历。
广度优先遍历是很多算法的原型。
比如拓扑排序的kahn算法, Prim最小生成树算法, 单源最短路径算法Dijsktra。
单单论广度优先遍历的变种:
朴素广度优先遍历 => 按层广度优先遍历.
多源广度优先遍历;
01BFS;
bfs与dfs结合生成路径;
bfs剪枝策略;
双向广搜;
本篇只介绍基础部分的内容
广度优先遍历
以下内容均用bfs代替广度优先遍历。
用途:
- 遍历图中所有的顶点。
- 遍历所有顶点的路径。
- 无向不带权图, 或者带权图权重相同且权重为正值), 求两点之间的最短路径。
- 图的连通性检验, 图连通分量求解
- kahn,Prim,Dijsktra… 图算法都是bfs的变种实现。
下面是无向图的bfs
具体图解一下(平板上手写字有点丑, 对不起对不起对不起):
以如下图为例
bfs在于选择一个源节点开始扩散, 这里以f为例。
先将f的邻接点b,e,c,g遍历一遍。然后再对b,e,c,g邻接且为访问过的顶点进行遍历。
这里实现需要借助队列(先进先出的结构), 和哈希集合(用于记忆化搜索,避免重复遍历, 优化效率)。
总而言之就是入队列是将这个点加入哈希集合中, 出队列就把它所有的未在哈希集合顶点加入到队列。
算法流程和代码实现在下文处有详解。
如果以最初的源头节点f标记为第一层,那么b,e,c,g就是第二层,a,d在第三层, 可以得到如下广度优先树。
这棵树是bfs在无向图中以源节点f的生成树。
这里简单给几个结论吧。
- 无向连通图中, 以某个节点扩散, 会得到以该节点为根节点的生成树(广度优先树)。
- 以src节点进行按层扩散, 朴素bfs不明显地区分层,但是按层的顺序。优化bfs(按层bfs)就把这个特征显示地展现了。=>这里类比经典二叉树层序遍历和优化层序遍历。
- 可以求解src到dest的最短边数, 比如,f->d点经过的最短边数是2。 需要处理f->d的所有简单路径然后挨个比大小吗? bfs当然不需要, 因为它总是处理完离源节点为k长度的所有节点,然后才处理k+1长度的节点。 bfs的性质。
- 前面提过,bfs可以解决最短路径问题。 不过这种最短路径的方法的使用条件必须记住。怎么说呢?毕竟bfs是相对"底层"的图算法, 最短路径算法有Bellman-Ford, A*, SPFA,Dijktra算法,它们的限制和使用条件也不尽相同。 我想说bfs使用限制应该相较于上述的最短路径算法要更加局限。
使用条件:1. 无权图:这种情况等价于3求最短边问题。 2:带权图: a->b且b->a的相互权重相同, 且权重不为负值,不需要所有边权重都统一。
说了这么多, 你可能会吐槽, “我怎么记得住呢?” 下面先介绍算法流程,代码实现,然后我会以bfs求解图中连通分量问题, 无向图判环, 有向图判环来辅助理解这些性质
广度优先遍历的基本实现
这里重申一遍上面用过的容器
- 需要队列这种数据结构辅助。
由于BFS需要由近到远的处理, 队列这种先进先出的结构可以总是让距离近的顶点先被处理。 因此广度优先遍历需要队列存储顶点和维护顺序。 - 需要哈希表(哈希集合)来标记已访问节点。
BFS适用于无向图和带环图, 必须标记已访问的节点, 否则可能会陷入死循环重复处理同一节点或者困于环中。有个名词,记忆化搜索, 就是这个。
以下是朴素广度优先遍历的Java实现
算法流程:
- 给定一个源节点s, 将其加入队列, 并用哈希表标记开始遍历。
- 弹出对头的节点,并扫描它的所有关联的顶点, 若当前关联顶点未标记过那么加入队列。
- 重复2过程直至队列为空, 弹出对头节点时可以进行额外处理(比如打印,收集…)。
三种建图方式实现
任选一种你喜欢的方式即可。
链式前向星
:
package BFS;
public class Code01_BFS {
//链式前向星的bfs写法
public static int MAXN = 1001;
public static int MAXM = 2003;
public static int[] head = new int[MAXN];
public static int[] next = new int[MAXM];
public static int[] to = new int[MAXM];
public static int cnt;
public static int[] queue = new int[MAXN];
public static int l,r;
public static boolean[] visited = new boolean[MAXN];
//把0编号空出来
public static void build(int n){
for(int i=1;i<=n;i++){
head[i] = 0;
visited[i] = false;
}
cnt = 1;
}
//添加一条有向边u->v
//无向边就交换参数多调一次这个。
public static void addEdge(int u,int v){
next[cnt] = head[u];
to[cnt] = v;
head[u] = cnt++;
}
public static void bfs(int start){
if(start == 0) {
return;
}
queue[r++] = start;
visited[start] = true;
while(l != r){
int cur = queue[l++];
//这里可以处理cur...打印,收集,处理信息...
System.out.print(cur+" ");
for(int ei = head[cur]; ei != 0;ei = next[ei]){
int dst = to[ei];
if(!visited[dst]){
queue[r++] = dst;
visited[dst] = true;//标记已访问过
}
}
}
}
// 主函数
public static void main(String[] args) {
build(6); // 假设图有6个节点
//这种图结构
//1 -> 2, 3
//2 -> 4, 5
//3 -> 6
addEdge(1, 2);
addEdge(1, 3);
addEdge(2, 4);
addEdge(2, 5);
addEdge(3, 6);
System.out.println("BFS traversal starting from node 1:");
bfs(1); // 从节点1开始BFS遍历
}
}
邻接矩阵
:
package BFS;
public class Code02_BFS_Matrix {
public static int MAXN = 101;
public static int[][] graph = new int[MAXN][MAXN];
public static int[] queue = new int[MAXN];
public static int l,r;
public static boolean[] visited = new boolean[MAXN];
public static int n;
public static void build(){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
graph[i][j] = 0;
}
visited[i] = false;
}
}
//添加有向不带权边。u->v
//无向边交换参数又调一次。
public static void addEdge(int u,int v){
graph[u][v] = 1;
}
public static void bfs(int start){
if(start == 0) {
return