数据结构试验迷宫问题_数据结构课程设计——迷宫求解(四)

本文介绍了使用广度优先搜索(BFS)解决迷宫问题,与深度优先搜索(DFS)进行对比。BFS通过层次遍历找到最短路径,而DFS偶尔更快,但可能找到非最短路线。代码实现中,BFS使用自定义队列,DFS涉及路径解析。两者在时间复杂度上相近,但在实际应用中表现不同。
摘要由CSDN通过智能技术生成
f50009560354b37f27f15cac7e8420f5.png前言

上一篇推文介绍了使用深度优先搜索求解迷宫出路,其实到这里课设的任务点就已经完成的差不多了,至于要求输出三元组和结果方阵,这些都比较简单,深搜方法有一个返回值,返回了一个栈的对象,栈内存储的就是三元组,即 Node 类的对象,只要稍加处理即可,所以没有对这一点进行过多描述。

本文将介绍另一种求解迷宫出路的方法——广度优先搜索 (BFS),并将其与深搜进行对比。

0 1广度优先搜索

广度优先搜索,也称宽度优先搜索,英文简写成 BFS。深搜我们比较熟悉,二叉树的先序遍历就是一个很好的例子,还有此前介绍过的求解全排列也是使用的深搜,广搜介绍的比较少,不过也有过介绍,如果读者还对树的几种遍历方式有印象的话,应当能够回忆起我们除了介绍过先序遍历、中序遍历和后序遍历以外,在最开始还介绍过树的层次遍历。(《Python数据结构——树(一)》)

树的层次遍历就是广搜,所以接下来关于广搜简单介绍如下。

访问当前位置

当前位置四周各个可以走的路口加入队列

队首出队,重复上述步骤

总结起来也就是,每个路口都要徘徊一下,整个前进过程是齐步推进的过程,对此如果不太理解,可以查看此前发布的一个视频《广度优先搜索求解迷宫的出路》。

此次不再单独介绍树的层次遍历,对此感兴趣或不太了解的读者,可以移步到《Python数据结构——树(一)》。

0 2前期工作

其实广度优先搜索找到迷宫出路之后,还需要多做一步,那就是解析出路线,可能有读者会想,“不是已经找到通往终点的路线了吗,为什么还要多做一步解析?”

实际上看过广搜求解迷宫问题的视频之后,就应该能够发现,在生成迷宫之后,先是有蓝色的矩形从起点向终点扩展过去,某一次扩展到达了终点之后,停止扩展,接下来从起点向终点会有一个新的图片标记出一条路线。所以我们最终要做到的效果应当如下图所示。

ddc43e7332ef16a78a10ee66180434fa.gif

在树的层次遍历中,我们使用了队列,在这里我们完成广搜也是使用队列,只不过这次的队列没有使用 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 的一个对象,接收了路径解析的返回值。

重新展示代码运行结果如下。

ddc43e7332ef16a78a10ee66180434fa.gif

视频《广度优先搜索求解迷宫的出路》

0 4BFS 与 DFS

我们知道深搜是很盲目的一条条路去尝试,但其实广搜也属于很盲目的求解方法,它对每个可能的路口都进行尝试,直到某个路口让它走到了终点。

从时间复杂度上分析,两种算法复杂度都很高,最差情况就是几乎要遍历完整个迷宫才找到出路,只不过时间复杂度毕竟描述的是最差情况,而经过多次测试发现,对于同一个有出口的迷宫,深搜在某次探测中途就找到出口的几率还是挺大的,甚至还存在少数次试探就找到出路的情况,不过这个又是小概率了。而广搜不一样,不论迷宫如何,几乎都要遍历完整个迷宫才能找到出口,不过如果迷宫有多种可能路线,广搜找到的是最短路线,而深搜就不一定了。

总的来说,深搜偶尔快,整体来说偏慢,虽然与广搜时间复杂度相同,但在完全随机生成的迷宫中,比广搜快一点的几率还是挺大的,至于广搜,可以说是慢的很稳定,没什么太突出的地方,不过在有多种可能路线的迷宫中,能够找到最短的出路。

END

在最后Last but not least

本文介绍了如何使用广度优先搜索求解迷宫的出路,附有详尽的代码与注释,同时也有相当多的文字对此加以说明,用于弥补此前对广搜介绍的不足。

广度优先搜索从实现上来说并不难,也欢迎感兴趣的读者对我的代码进行更多的优化。如果对代码不够理解,建议先看下树的层次遍历,用于辅助理解。

往期 精彩回顾

数据结构课程设计——迷宫求解(一)

数据结构课程设计——迷宫求解(二)

数据结构课程设计——迷宫求解(三)

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值