一、啥是广度优先搜索
广度优先搜索(BFS)就像是在一个大迷宫里找东西。咱们从起点开始,先看看离起点最近的那些地方,把这些地方都检查一遍,然后再去看离这些地方近的其他地方,一层一层地往外扩展。就好比你站在一个广场中间,先看看广场周围一圈的店铺,看完了再去看这些店铺旁边街道上的其他地方。
二、队列优化是怎么回事
在广度优先搜索里,队列就像是一个排队的队伍。咱们把要去探索的地方按照顺序排好队,每次从队伍最前面拿出一个地方去探索,然后把这个地方能到达的新地方都加到队伍的末尾。这样就能保证我们是一层一层地去探索,不会乱了顺序。
比如说,我们有一个图,里面有很多节点,每个节点都和其他一些节点相连。我们从一个节点开始,把这个节点放到队列里,然后开始探索。每次从队列里拿出一个节点,看看它能到达哪些新节点,把这些新节点都加到队列末尾。这样不断重复,直到队列为空。
三、实现图的层级遍历
我们来用 Java 实现图的层级遍历。这里我们用邻接表来表示图,邻接表就是一个数组,数组里每个元素是一个链表,链表存储了和这个节点相连的其他节点。
// Java 技术栈
import java.util.*;
// 图类
class Graph {
private int V; // 节点数量
private LinkedList<Integer> adj[]; // 邻接表
// 构造函数
Graph(int v) {
V = v;
adj = new LinkedList[v];
for (int i = 0; i < v; ++i)
adj[i] = new LinkedList();
}
// 添加边
void addEdge(int v, int w) {
adj[v].add(w);
}
// 层级遍历
void BFS(int s) {
// 标记节点是否被访问过
boolean visited[] = new boolean[V];
// 创建队列
LinkedList<Integer> queue = new LinkedList<Integer>();
// 标记当前节点为已访问并加入队列
visited[s] = true;
queue.add(s);
while (queue.size() != 0) {
// 从队列头部取出一个节点
s = queue.poll();
System.out.print(s + " ");
// 获取当前节点的所有邻接节点
Iterator<Integer> i = adj[s].listIterator();
while (i.hasNext()) {
int n = i.next();
if (!visited[n]) {
// 标记未访问的邻接节点为已访问并加入队列
visited[n] = true;
queue.add(n);
}
}
}
}
public static void main(String args[]) {
// 创建一个有 4 个节点的图
Graph g = new Graph(4);
// 添加边
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
g.addEdge(3, 3);
System.out.println("从节点 2 开始的层级遍历结果:");
g.BFS(2);
}
}
在这个例子里,我们首先创建了一个图,然后添加了一些边。接着从节点 2 开始进行层级遍历,把访问过的节点依次打印出来。
四、求解最短路径
广度优先搜索还可以用来求解最短路径。因为它是一层一层地扩展,所以第一次到达某个节点时经过的路径就是最短路径。
我们还是用上面的图,稍微修改一下代码来求解最短路径。
// Java 技术栈
import java.util.*;
// 图类
class Graph {
private int V; // 节点数量
private LinkedList<Integer> adj[]; // 邻接表
// 构造函数
Graph(int v) {
V = v;
adj = new LinkedList[v];
for (int i = 0; i < v; ++i)
adj[i] = new LinkedList();
}
// 添加边
void addEdge(int v, int w) {
adj[v].add(w);
}
// 求解最短路径
void shortestPath(int start, int end) {
// 标记节点是否被访问过
boolean visited[] = new boolean[V];
// 存储每个节点的前一个节点
int[] prev = new int[V];
// 创建队列
LinkedList<Integer> queue = new LinkedList<Integer>();
// 标记当前节点为已访问并加入队列
visited[start] = true;
queue.add(start);
prev[start] = -1;
while (queue.size() != 0) {
// 从队列头部取出一个节点
int s = queue.poll();
// 如果到达目标节点,结束搜索
if (s == end) {
break;
}
// 获取当前节点的所有邻接节点
Iterator<Integer> i = adj[s].listIterator();
while (i.hasNext()) {
int n = i.next();
if (!visited[n]) {
// 标记未访问的邻接节点为已访问并加入队列
visited[n] = true;
queue.add(n);
prev[n] = s;
}
}
}
// 从目标节点回溯到起始节点
List<Integer> path = new ArrayList<>();
for (int at = end; at != -1; at = prev[at]) {
path.add(at);
}
Collections.reverse(path);
if (path.get(0) != start) {
System.out.println("没有找到路径");
} else {
System.out.println("最短路径: " + path);
}
}
public static void main(String args[]) {
// 创建一个有 4 个节点的图
Graph g = new Graph(4);
// 添加边
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
g.addEdge(3, 3);
System.out.println("从节点 0 到节点 3 的最短路径:");
g.shortestPath(0, 3);
}
}
在这个例子里,我们用一个数组 prev 来存储每个节点的前一个节点,这样就能通过回溯找到最短路径。
五、应用场景
广度优先搜索的应用场景有很多。比如说在社交网络里,我们可以用它来找到两个用户之间的最短关系链。在游戏里,我们可以用它来找到角色从一个位置到另一个位置的最短路径。在地图导航里,也可以用它来规划最短路线。
六、技术优缺点
优点
- 简单易懂:广度优先搜索的逻辑比较简单,很容易理解和实现。
- 能找到最短路径:因为它是一层一层地扩展,所以能保证找到的路径是最短的。
- 适用于无权图:对于没有边权的图,广度优先搜索是一个很好的选择。
缺点
- 空间复杂度高:需要使用队列来存储待探索的节点,当图很大时,队列可能会占用很多内存。
- 不适用于带权图:如果图中的边有不同的权重,广度优先搜索就不能找到最优路径了,需要用其他算法,比如 Dijkstra 算法。
七、注意事项
- 节点标记:在遍历过程中,一定要标记节点是否被访问过,否则会出现重复访问的情况,导致无限循环。
- 队列管理:队列的操作要正确,确保先加入的节点先被访问。
- 图的表示:要根据图的特点选择合适的表示方法,比如邻接表或邻接矩阵。
八、文章总结
广度优先搜索是一种非常实用的算法,通过队列优化可以实现图的层级遍历和最短路径求解。它的逻辑简单,能解决很多实际问题,比如社交网络分析、游戏路径规划等。不过它也有一些缺点,比如空间复杂度高和不适用于带权图。在使用时,我们要注意节点标记、队列管理和图的表示等问题。
评论