![f50009560354b37f27f15cac7e8420f5.png](https://i-blog.csdnimg.cn/blog_migrate/b89a4f012f5ba8ed01553e04adbe9762.png)
上一篇推文介绍了使用深度优先搜索求解迷宫出路,其实到这里课设的任务点就已经完成的差不多了,至于要求输出三元组和结果方阵,这些都比较简单,深搜方法有一个返回值,返回了一个栈的对象,栈内存储的就是三元组,即 Node 类的对象,只要稍加处理即可,所以没有对这一点进行过多描述。
本文将介绍另一种求解迷宫出路的方法——广度优先搜索 (BFS),并将其与深搜进行对比。
0 1广度优先搜索广度优先搜索,也称宽度优先搜索,英文简写成 BFS。深搜我们比较熟悉,二叉树的先序遍历就是一个很好的例子,还有此前介绍过的求解全排列也是使用的深搜,广搜介绍的比较少,不过也有过介绍,如果读者还对树的几种遍历方式有印象的话,应当能够回忆起我们除了介绍过先序遍历、中序遍历和后序遍历以外,在最开始还介绍过树的层次遍历。(《Python数据结构——树(一)》)
树的层次遍历就是广搜,所以接下来关于广搜简单介绍如下。
访问当前位置
当前位置四周各个可以走的路口加入队列
队首出队,重复上述步骤
总结起来也就是,每个路口都要徘徊一下,整个前进过程是齐步推进的过程,对此如果不太理解,可以查看此前发布的一个视频《广度优先搜索求解迷宫的出路》。
此次不再单独介绍树的层次遍历,对此感兴趣或不太了解的读者,可以移步到《Python数据结构——树(一)》。
0 2前期工作其实广度优先搜索找到迷宫出路之后,还需要多做一步,那就是解析出路线,可能有读者会想,“不是已经找到通往终点的路线了吗,为什么还要多做一步解析?”
实际上看过广搜求解迷宫问题的视频之后,就应该能够发现,在生成迷宫之后,先是有蓝色的矩形从起点向终点扩展过去,某一次扩展到达了终点之后,停止扩展,接下来从起点向终点会有一个新的图片标记出一条路线。所以我们最终要做到的效果应当如下图所示。
在树的层次遍历中,我们使用了队列,在这里我们完成广搜也是使用队列,只不过这次的队列没有使用 Java 中自带的,这次是自己实现的,如果看过往期推文中关于栈和队列部分,相信实现这个数据结构应该不难。
队列的代码如下。
// 队列类,实为带首尾结点指针的单向链表
public class Queue {
QueueNode front; // 队首指针
QueueNode rear; // 队尾指针
int length;
// 队列构造函数,创建一个空队列
public Queue(){
this.front = null;
this.rear = null;
this.length = 0;
}
// 新结点加入队尾
public void enqueue(Node node){
QueueNode lnode = new QueueNode(node, null);
if(this.rear==null){ // 队列为空时插入
this.rear = lnode;
this.front = lnode;
}else{ // 链表非空时尾插
this.rear.next = lnode;
this.rear = this.rear.next;
}
this.length = this.length + 1;
}
// 队首结点出队
public Node dequeue(){
QueueNode h = this.front;
if(h!=null) { // 队列非空
if(this.length==1){ // 队列只有一个结点
this.front = null;
this.rear = null;
}else { // 队列不止一个结点
this.front = this.front.next;
h.next = null;
}
this.length = this.length - 1;
return h.data;
}
else{ // 暂不设置异常抛出
return null; // 队列为空
}
}
// 判断队列是否为空
public boolean isEmpty(){
return this.length == 0;
}
}
// 队列结点类
class QueueNode {
Node data;
QueueNode next; // 后继
public QueueNode(Node data, QueueNode next) {
this.data = data;
this.next = next;
}
}
代码有点长,但不难理解,简单描述就是,这里实际使用了一个带首尾指针 (Java 中没有指针概念,这样说只是表述方便) 的单链表,在链表首部执行出队操作,链表尾部执行入队操作,链表结点类的数据域为 Node 类 (三元组,) 的对象,指针域用于指示下一个结点。
0 3广度优先搜索求解迷宫出路在广搜过程中,图形绘制也使用了休眠函数,便于观察动态过程,此处不再提供,不熟悉的读者可以划到文章底部,查看上一篇推文。
从前期工作中,我们已经自行实现了队列,可以用于广度优先搜索,但是提出了一个需要找到终点后解析出路径的新问题,广搜没有回溯,所以如果不解析出路径,最终只能看到几乎整个迷宫都是被蓝色标记过的方块,这样并没有达到求解出路的目的。
实际上可能会有读者想到,能否设置一个栈或者数组之类的,用于保存中间结果。这个思路很好,但是到底保存什么呢?
我最终采取的方法是,设置一个新的二维数组,类型是 String,用于存储移动方向,这样到达终点以后,只需要根据这个存有移动方向的矩阵就可以反向推解出路径,并把路径压栈,返回一个栈的对象就可以了,接下来只要在主方法里接收返回值并访问该对象即可。
例如,从坐标 (1, 1) 向右移动到达 (1, 2) ,那么路径矩阵 movepath 就可以这样赋值:movepath[1][2] = "Right",即 (1, 2) 是由某个位置向右移动而得到的,反解的时候只需要对其反向移动,也就是修改坐标即可推出上一个坐标。从终点逐步推导,一直反解到起点就可以了。
路径解析函数如下:
// BFS 结果解析函数
public static Stack parsePath(String movepath[][], int x, int y) { // 对广搜结果进行反解,求出路线
Stack stack = new Stack();
stack.push(new Node(x, y, "END"));
while (!(x == 1 && y == 1)) {
switch (movepath[x][y]) {
case "Up": x = x + 1; stack.push(new Node(x, y, "Up")); break;
case "Down": x = x - 1; stack.push(new Node(x, y, "Down")); break;
case "Left": y = y + 1; stack.push(new Node(x, y, "Left")); break;
case "Right": y = y - 1; stack.push(new Node(x, y, "Right")); break;
}
}
return stack;
}
这里使用的 switch-case 语句,如果学过 C/C++ 或 Java 应该能够理解,不了解也没关系,就把这看成是四个 if 语句就行。另外此处的书写格式只是为了在微信文章中压缩行数,实际书写不建议写成一行。
如果直接在路径解析函数中,解析出一个路径就画一个图案,而不返回栈对象,也可以实现同样的效果,只不过由于是从终点逆向推出路线,所以会从终点开始画。
现在要处理的内容都已经有了,接下来就是广度优先搜索的主要代码了,代码比较冗长,如果读者算法学的还可以,可以尝试对其进行优化,此处我就直接展示我当时写的版本。
// BFS 非递归程序
public static Stack findWayByBFS(int maze[][],int start_x, int start_y, int end_x, int end_y, int level) {
Queue queue = new Queue();
Stack res_stack = null;
Node node;
Node front;
String moveto; // 记录当前走向
boolean moved;
String movepath[][] = new String[end_x+1][end_y+1];
if (start_x 1][start_y] == 0) { // 向下试探
node = new Node(start_x, start_y, "Down");
queue.enqueue(node);
}
if (start_y 1] == 0) { // 向右试探
node = new Node(start_x, start_y, "Right");
queue.enqueue(node);
}
while (!queue.isEmpty()) { // 队列不为空
moved = false;
front = queue.dequeue(); // 队首出队
start_x = front.x;
start_y = front.y;
maze[start_x][start_y] = -1;
// 访问队首结点
DrawMaze.drawMaze.drawRect(front.x, front.y, level, -1); // 广搜使用矩形进行示意,-1 为尝试访问的标记
sleep(100L - level * 10L); // 先暂停再继续
switch (front.direction) { // 队首开始移动
case "Up": start_x = start_x - 1; moveto = "Up"; break;
case "Down": start_x = start_x + 1; moveto = "Down"; break;
case "Left": start_y = start_y - 1; moveto = "Left"; break;
case "Right": start_y = start_y + 1; moveto = "Right"; break;
default: continue;
}
movepath[start_x][start_y] = moveto;
if (start_x == end_x && start_y == end_y) { // 到达终点
DrawMaze.drawMaze.drawRect(start_x, start_y, level, -1);
sleep(100L - level * 10L);
res_stack = parsePath(movepath, start_x, start_y); // 解析路径
break;
}
if (start_x - 1 >= 0 && maze[start_x - 1][start_y] == 0) { // 向上试探
node = new Node(start_x, start_y, "Up");
queue.enqueue(node);
moved = true;
}
if (start_x 1][start_y] == 0) { // 向下试探
node = new Node(start_x, start_y, "Down");
queue.enqueue(node);
moved = true;
}
if (start_y - 1 >= 0 && maze[start_x][start_y - 1] == 0) { // 向左试探
node = new Node(start_x, start_y, "Left");
queue.enqueue(node);
moved = true;
}
if (start_y 1] == 0) { // 向右试探
node = new Node(start_x, start_y, "Right");
queue.enqueue(node);
moved = true;
}
if (!moved && maze[start_x][start_y] == 0) // 没有移动,且当前值为 0,即地面,说明进入死胡同
queue.enqueue(new Node(start_x, start_y, "Death")); // 死路也需要标记,便于可视化
}
return res_stack;
}
主要的还是对上下左右四个方向试探,需要记得将访问过的点标记为 -1,以免重复访问造成死循环。具体的广搜思路在本文开头有过介绍,可以对照思路查看代码,另外代码也是先根据思路写出来,再在实际运行中改进的,可能有部分添加的新内容,但不会影响到对代码的理解。level 参数重提一下,最终的课设我设计了三种难度的迷宫,所以这里会有一个 level 代表当前迷宫的等级,测试时候传参为 1 ,默认 1 级即可。
主方法中实现的绘制最终线路代码如下,实际课设中,重新实现了栈,这一部分代码是封装成栈的一个方法的。此处只是单独测试 BFS ,就不考虑封装了。
Node node;
while(!stack.isEmpty()){
node = (Node)stack.pop();
FindWayByBFS.sleep(100L); // 休眠,便于动态演示
DrawMaze.drawMaze.drawLattice(node.x, node.y, 1, -1); // 画出结果路径
System.out.println(" + node.x + ", " + node.y + ", " + node.direction + " >"); // 输出三元组
}
DrawMaze.drawMaze.drawLattice 就是演示视频中的最终贴图片这一步,如果对图形界面不熟悉,可以忽略这一步,直接使用打印语句输出三元组即可。上述代码中的 stack 是 Stack 的一个对象,接收了路径解析的返回值。
重新展示代码运行结果如下。
视频《广度优先搜索求解迷宫的出路》
0 4BFS 与 DFS我们知道深搜是很盲目的一条条路去尝试,但其实广搜也属于很盲目的求解方法,它对每个可能的路口都进行尝试,直到某个路口让它走到了终点。
从时间复杂度上分析,两种算法复杂度都很高,最差情况就是几乎要遍历完整个迷宫才找到出路,只不过时间复杂度毕竟描述的是最差情况,而经过多次测试发现,对于同一个有出口的迷宫,深搜在某次探测中途就找到出口的几率还是挺大的,甚至还存在少数次试探就找到出路的情况,不过这个又是小概率了。而广搜不一样,不论迷宫如何,几乎都要遍历完整个迷宫才能找到出口,不过如果迷宫有多种可能路线,广搜找到的是最短路线,而深搜就不一定了。
总的来说,深搜偶尔快,整体来说偏慢,虽然与广搜时间复杂度相同,但在完全随机生成的迷宫中,比广搜快一点的几率还是挺大的,至于广搜,可以说是慢的很稳定,没什么太突出的地方,不过在有多种可能路线的迷宫中,能够找到最短的出路。
END在最后Last but not least
本文介绍了如何使用广度优先搜索求解迷宫的出路,附有详尽的代码与注释,同时也有相当多的文字对此加以说明,用于弥补此前对广搜介绍的不足。
广度优先搜索从实现上来说并不难,也欢迎感兴趣的读者对我的代码进行更多的优化。如果对代码不够理解,建议先看下树的层次遍历,用于辅助理解。
往期 精彩回顾数据结构课程设计——迷宫求解(一)
数据结构课程设计——迷宫求解(二)
数据结构课程设计——迷宫求解(三)