揭开广度优先遍历BFS的神秘面纱

  • 前言
  • 1. 审题
    • 1.1 树的BFS
    • 1.2 图的BFS
  • 2. 解法
    • 2.1 树的BFS
    • 2.2 图的BFS
  • 3. 总结

前言

最近到了面试的高峰时期,前段时间也刷了不少广度优先遍历的算法题,算是对广度优先遍历有了一点点的理解与思考吧,希望在此记录下来,为后人以及未来的自己提供一个回忆的捷径~


1. 审题

既然是使用广度优先算法,那么首先得明白遇到的题目是否能够使用BFS,这又分为两种情况,一个是树的层次遍历,一个是有关图的遍历(包括邻接矩阵和链表),下面我将分别对这两种情况进行说明。

1.1 树的BFS

经常遇到的树的层次遍历一般就是用到BFS,比如统计某个节点的深度,统计所有节点的总和(DFS也可),统计叶子节点的总和(DFS也可),以及拷贝二叉树和二叉树的序列化,这些题目都属于同一种类型,解决方法也都是异曲同工之妙,解法将在下面进行介绍。

1.2 图的BFS

图的BFS规律特别好找,虽然一般审题上经常感觉是动态规划(确实有不少可以用dp去解决),如果你有幸学过编译原理,那么对有限自动机有所了解,有限自动机就是遇到一个新的字母或者数值会从当前状态跳到下一个状态,一般从初始状态到最终状态有多条路径,不同路径的步数可能相同可能不同,但是都会有最短的路径,即步数最少。图的BFS也是同样道理,经常是求比如图中从一个点到另一个点的最短路径,也就是从初始状态到目标状态的步数,下面是解法的详细介绍。

2. 解法

使用过BFS的朋友都会有这样的认识,即BFS就是通过队列实现的,这确实是BFS的核心,一般都是将初始位置或者节点放入队列中,以队列非空为条件,不断循环,一直到得到最后的结果退出或者队列空退出。

2.1 树的BFS

在树的BFS中,通常是将树的根节点放入到定义好的queue<TreeNode*> q中,然后不断遍历左右子树,如果存在则放入到队列中,循环遍历直到队列为空,中间的操作步骤根据题意进行即可,下面将举一个例子强化记忆。
比如LeetCode 297题,是关于二叉树的序列化过程,其实就是将二叉树转成字符串数组的形式存起来,那对于这个过程很容易想到层次遍历的方式,即使用广度优先遍历,过程如下:

  1. 利用队列广度优先遍历二叉树;
  2. 所遍历到的无论是否空的节点都加入到string中,空节点用null标记,节点之间用,分割;
  3. 最终得到的string是序列化的结果。

代码如下:

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Codec {public:// Encodes a tree to a single string.string serialize(TreeNode* root) {string res;// 如果根为空if(root == nullptr) {return res;} queue<TreeNode*> q;q.push(root);while(!q.empty()) {TreeNode* demo = q.front();q.pop();if(demo != nullptr) {res += to_string(demo -> val) + ",";q.push(demo -> left);q.push(demo -> right);} else {res += "null,";}}return res;}// Decodes your encoded data to tree.TreeNode* deserialize(string data) {if(data == "") {return nullptr;}vector<string> vals;// 分割字符串string s;for(int i = 0; i < data.size(); i ++) {if(data[i] == ',') {vals.push_back(s);s = "";} else {s += data[i];}}TreeNode* root = new TreeNode(atoi(vals[0].c_str()));queue<TreeNode*> q;q.push(root);// 标记当前根的位置int i = 0;while(!q.empty()) {TreeNode* demo = q.front();q.pop();// 有左节点if(vals[i * 2 + 1] != "null") {demo -> left = new TreeNode(atoi(vals[i * 2 + 1].c_str()));q.push(demo -> left);}// 有右节点if(vals[i * 2 + 2] != "null") {demo -> right = new TreeNode(atoi(vals[i * 2 + 2].c_str()));q.push(demo -> right);}i ++;}return root;}
};// Your Codec object will be instantiated and called as such:
// Codec codec;
// codec.deserialize(codec.serialize(root));

观察代码中serialize函数部分,首先把根节点放入队列中,然后循环遍历队列,然后在将左右子节点放入队列,如此反复,中间掺杂着把节点的值存入字符串中的操作,最后便能得出层次遍历的二叉树用string存储的结果。

2.2 图的BFS

图的BFS正如我在审题部分解释那样,队列中存放的节点表示处于同一状态的节点,一旦某个节点可以到达目的状态,那么此时的步数(状态从初始开始变换的次数)就是到达目的状态的最短长度,所以就是不断遍历队列中的数,并把获得的下一状态装入队列中,每次到达新的状态,step(步数)就要更新,一直到达目的状态,返回此时的step。注意,每次遍历过一个点都要记得标记。下面举几个例子。
第一个例子是最经典的二进制矩阵中的最短路径问题,从左上角出发,到达右下角位置,求最短路径,左上角是初始状态,入队列,其右、下、右下是它所能到的下一个状态(还要判断一下是否是0),全部放入队列中,step+1,再次遍历新的状态,如此反复,直到到达右下角,返回step,代码如下:

class Solution {public:int shortestPathBinaryMatrix(vector<vector<int>>& grid) {// 所给的地图为空if(grid.size() == 0) {return -1;}// 确定方向int direction[8][2] = {{1, -1}, {1, 0}, {1, 1}, {0, 1}, {0, -1}, {-1, -1}, {-1, 0}, {-1, 1}};int m = grid.size(), n = grid[0].size();// 用队列存储每一轮能够经过的地方queue<vector<int>> q;   // 把第一个点放进去    q.push({0, 0});int length = 0;// 如果队列不为空,说明还能走,空了说明走不下去了直接返回-1while(!q.empty()) {// 每一轮步长+1length ++;int size = q.size();// 遍历每一轮中能够走到的位置while(-- size >= 0) {vector<int> site = q.front();q.pop();int x = site[0], y = site[1];// 如果不能走if(grid[x][y] == 1) {continue;}// 如果走到右下角if(x == m - 1 && y == n - 1) {return length;}// 走过的位置标记为不能走grid[x][y] = 1;// 八个方向遍历下一步能走的位置for(auto d : direction) {int x1 = x + d[0], y1 = y + d[1];if(x1 < 0 || x1 >= m || y1 < 0 || y1 >= n) {continue;}if(grid[x1][y1] == 1) {continue;}q.push({x1, y1});}}}return -1;}
};/*作者:heroding
链接:https://leetcode-cn.com/problems/shortest-path-in-binary-matrix/solution/c-bfsdui-lie-by-heroding-biko/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。*/

几乎每一行都有注释,我想自己看完这些注释大家都可以明白,所以这里我不再对代码细说了。

第二个例子本来想说蛇梯棋,但是该题的本质和上一个例子完全一样,只是换成了左下角到右上角(或者左上角),只不过多了一个蛇梯子的判断,以及蛇形的序列与数组序列的转换问题,所以不细说了。重点说的是另一种应用,就是与Map结合的方式,LeetCode 815 公交线路就是我想说的这种问题,与单纯的点集中不同方向遍历不同,这种线路遍历相当于行式遍历,每次遍历都是一整个线路,所以标记的话就直接按照行(线路)标记了,Map的作用在这里是为了统计经过同一个点的线路有哪些,代码如下:

class Solution {public:int numBusesToDestination(vector<vector<int>>& routes, int source, int target) {// 如果起点终点相同if(source == target) {return 0;}// 标记站点出现的线路unordered_map<int, vector<int>> mp;// 标记访问的线路vector<bool> visited(routes.size(), false);// 记录站点出现的线路for(int i = 0; i < routes.size(); i ++) {for(int j = 0; j < routes[i].size(); j ++) {mp[routes[i][j]].push_back(i);}}// 定义队列并初始化queue<int> q;q.push(source);int step = 0;while(!q.empty()) {step ++;int len = q.size();for(int i = 0; i < len; i ++) {int site = q.front();q.pop();for(int& r : mp[site]) {if(!visited[r]) {for(int j = 0; j < routes[r].size(); j ++) {if(routes[r][j] == target) {return step;}q.push(routes[r][j]);}visited[r] = true;}}}}return -1;}
};/*作者:heroding
链接:https://leetcode-cn.com/problems/bus-routes/solution/cbfs-mapxiang-jie-by-heroding-9we4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。*/

观察代码可以感受到这道题目的趣味性,为啥和以前按照点遍历不一样,如果按照点不能处理吗?能,但是特别占用内存和耗时,因为这里不能对访问过的站点标记,你标记完了,别的路线不能访问了,这样不标记的方法肯定会导致时间复杂度爆炸。

3. 总结

本文中我将BFS的方法总结为树和图两种,可能会因为我见识短浅接触过少从而有其他的数据结构类型也可以使用BFS,在此深感抱歉,希望读者可以指出,我将对此进行更新。但是不得不说,如上所述方法可以应对大部分的BFS题型,套路模板都是一样的,直接套用即可,只要读者耐心读下去,一定能有所收获,当然实践是检验真知的唯一手段,还是需要大家勤练习,才能真正掌握BFS的精妙之处,从而以不变应万变。

算法笔记 揭开广度优先遍历BFS的神秘面纱 HERODING的算法之路相关推荐

  1. 图论算法(5):图的广度优先遍历 BFS

    本章节内容使用 java 实现,Github 代码仓:https://github.com/ZhekaiLi/Code/tree/main/Graph/src 查看文章内的图片可能需要科学上网! 因为 ...

  2. 数据结构与算法(7-2)图的遍历(深度优先遍历DFS、广度优先遍历BFS)(分别用邻接矩阵和邻接表实现)

    目录 深度优先遍历(DFS)和广度优先遍历(BFS)原理 1.自己的原理图 2.官方原理图 一.邻接矩阵的深度优先遍历(DFS) 1.原理图 2. 过程: 3.总代码 二.邻接表的深度优先遍历(DFS ...

  3. 揭开J2EE集群的神秘面纱(一):什么是J2EE集群

    揭开J2EE集群的神秘面纱(一):什么是J2EE集群 作者:不详 来源:CSDN博客 酷勤网收集 2008-04-18 摘要 酷勤网 J2EE集群技术包括负载均衡和失效转移.负载均衡意味着有许多客户端 ...

  4. 连通子图什么意思_一道物理竞赛题揭开“希罗喷泉”的神秘面纱,到底什么物理原理?...

    多数初二的中学生朋友们现在都已经在学习压强知识,这一块知识属于中考物理必考重点内容,所占分值很高,所考查题型非常广泛,而且多是力学知识的综合性应用,因此属于中考的一个难点,中学生朋友们在遇到有关此类复 ...

  5. xpress-mp优化实例精选_实例解析,揭开“隔震技术”的神秘面纱!

    作者简介:仁者见仁,从事施工管理,深扎施工一线多年.著有微信公众号"仁者见仁",旨在为大家提供关于施工技术.项目管理最接地气的满满干货. 传统的抗震技术主要特点是"抗&q ...

  6. 揭开RTMP播放流程的神秘面纱

    RTMP 是目前各种网络直播应用最核心的传输协议,也是互动直播采用最广泛的协议. 如果说流媒体服务器(Server)是网络直播的骨骼,RTMP则是网络直播的血液,可以说,没有RTMP,就没有今天如此火 ...

  7. 【算法】广度优先遍历 (BFS)

    目录 1.概述 2.代码实现 3.应用 1.概述 (1)广度优先遍历 (Breadth First Search),又称宽度优先遍历,是最简便的图的搜索算法之一. (2)已知图 G = (V, E) ...

  8. Java数据结构之图的基本概念和算法,深度优先遍历DFS,广度优先遍历BFS(图解)

    文章目录 前言 一.图的基本概念 1.图的定义 2.基本术语 二.图的基本算法 1.初始化图 2.插入顶点和边 3.矩阵打印 4.返回第一个邻接结点的下标 5.返回第一个邻接结点的下一个结点的下标 三 ...

  9. java数据结构和算法——图的广度优先(BFS)遍历

    目录 一.图的遍历介绍 二.图的广度优先搜索(Broad First Search) 三.图的广度优先遍历算法步骤 四.图的广度优先遍历示例需求 五.图的广度优先遍历代码示例 一.图的遍历介绍 所谓图 ...

最新文章

  1. C#编写的生成缩略图程序
  2. SDK安装报错HTTP Status 416
  3. [转]Linux配置防火墙
  4. mysql 切表_mysql--------命令来操作表
  5. locate 命令详解
  6. 【数据迁移】使用传输表空间迁移数据
  7. [中级]Java命令学习系列(五)——jhat
  8. ibatis oracle function,IBATIS调用oracle function(函数)的步骤实例
  9. [20170616]vim 8.0的安装.txt
  10. 利用python在word文档中查找关键字(支持多个文档和多个关键字)
  11. VMware彻底删除、扫描添加导入,已安装好的虚拟计算机
  12. 千锋php 靠谱吗,千锋PHP学员心得 长久的坚持不懈才能接近成功
  13. LED电子时钟显示屏(NTP时间同步服务器)是如何完成授时服务的?
  14. 左手技术,右手生态 英特尔如何打响名为“数据”的战争?
  15. iOS获取WIFI配置信息,WIFI名称、网关(路由器地址)、本机IP地址、DNS
  16. 前端自学之HTML(02)
  17. 如何提取王者荣耀模型
  18. 从互联网角度出发,慧算账受客户追捧
  19. 基于matlab的控制系统仿真题,MATLAB与控制系统仿期末考试试卷真
  20. Rtos的调研分析报告

热门文章

  1. Sicily 1466. Taunt Exposure Estimation
  2. 一文讲清支付资金清算系统的功能
  3. ​QGIS Cloud 一个基于云的 GIS 平台
  4. 野火F1开发板STM32案例-MultiButton移植
  5. DDD 领域驱动设计 - 架构(分层/六边形/RESTful)
  6. Liang-GaRy啃linux书想吐(六)
  7. 结对编程——《构建之法》读书笔记
  8. ionic 打 android 出现 Current working directory is not a Cordova-based project.
  9. 一个实战案例带你走完python数据分析全流程:豆瓣电影评论的关键词云图制作
  10. matlab工具箱分析关节力矩,在matlab工具箱中,以下选项中哪个方法是用来计算动力学力矩?...