壹 ❀ 引

我在从JS执行栈角度图解递归以及二叉树的前、中、后遍历的底层差异一文中,从一个最基本的数组遍历引出递归,在掌握递归的书写规则后,又从JS执行栈角度解释了二叉树三种深度优先(前序、中序后序)的底层差异,帮助大家站在模板的角度上深入理解模板。而二叉树还剩一种广度优先(也称层序遍历)也使用广泛,但考虑到篇幅问题,所以还是打算另开一篇文章讲解。

其实相对深度优先,广度优先的模板要好理解的一些,毕竟它没有让人头疼的递归,且我们只用维护好一个队列queue(先进先出)即可,但考虑到刚接触算法人基础不同,我们还是用图解以及更直白的话术来讲解层序遍历的模板,那么本文开始。

贰 ❀ 壹 从队列与栈说起

其实通过前文二叉树深度遍历的讲解,所谓前序、中序与后序遍历的差异,本质上是将做一件事的动作放在了递归前后不同位置,而这个原理其实依赖了JS执行栈先进后出的特性,这是JS底层帮我们做的事。那我们自己能不能模拟栈呢?显然是可以的,通过数组的pushpop方法,比如数字1 2 3我可以先全部push到一个空数组中变成[1, 2, 3],然后再利用pop依次弹出就能倒序访问变成3 2 1

const stack = [];
// 先进
stack.push(...[1, 2, 3]); while (stack.length > 0) {
// 后出
console.log(stack.pop());// 3 2 1
}

用图来表示这个过程:

而栈的特性是先进后出,我们是不是一样可以用数组模拟这个过程,别忘了数组还有shift方法:

const stack = [];
stack.push(...[1, 2, 3]); while (stack.length > 0) {
console.log(stack.shift()); // 1 2 3
};

理解了用数组模拟队列的过程,那么层序遍历就理解了一半了。

叁 ❀ 图解层序遍历

我们来看一个最基本的二叉树层序遍历例子,如下,如果要层序遍历输出自然是[1,2,3,4,5,6,7],那么怎么做?

固定套路,我们可以先定义一个承装结果的数组ans,以及一个模拟队列的数组queue,整个遍历的过程都会检查queue的长度,只要length > 0证明还有元素,那就标明可以继续做我们要做的事。

很明显面对上图的二叉树,第一次层自然是根节点,那我们就可以将根节点先塞到queue中:

然后开始遍历queue做我们要做的操作,做什么呢?当然是将当前节点的值存起来,之后因为节点1已经访问过了,遵循先进先出的特性,我们需要将1从数组头部弹出。那么问题来了,1要是弹出queue长度直接为0遍历结束,但我们希望继续检查第二层,我们是不是得将1的子节点依次也pushqueue中,于是就有了下面这一步,将2 3入队列,并将1弹出:

同理,因为当前queue长度为2,很明显我们能继续遍历,依次将2,3存入ans数组,之后再将2的子节点4,53的子节点6,7存入queue,同时2,3出队列。

之后我们再将4,5,6,7的值存入ans,但由于这四个节点没有自己的子节点,所以我们只用让它们四个出队列,也由于没有新的节点进来,queue长度为空,终于整个查找过程结束,我们顺序得到ans [1,2,3,4,5,6,7]

在通过图解解释了这个过程后,我们就能很清晰定义出层序遍历的模板:

const levelTraverse = (root) => {
if (root === null) return;
const queue = [];
const ans = [];
queue.push(root);
// 当队列为空时跳出遍历
while (queue.length > 0) {
// 当前节点出队列
const node = queue.shift();
// 做你要做的操作
ans.push(node.val);
// 别忘了让node的左右子节点进入队列
// 因为不是递归了,加入前还是有必要检查有没有子节点
if (node.left !== null) {
queue.push(node.left);
}
if (node.right !== null) {
queue.push(node.right);
}
};
return ans;
}

肆 ❀ 一道基础的层序遍历面试题

那么图解完层序遍历模板,我们趁热打铁来做道题。我现在公司有一个同事一直负责他们部门的前端招聘工作,有一次他跟我说今天一个来面试候选人有点厉害,算法10分钟就写出来了,我出于兴趣就问他考的什么算法题?于是他就把题目发给我了,题目如下:

// 树结构的广度优先遍历(BFS)-(建议做题时长:20分钟)
// 从后端拿到了一个文档节点树结构,请按广度优先遍历(BFS)的顺序,返回该树结构的所有节点名字。 // 树节点列表的数据说明: // 节点定义
const NodeType = {
name: '<Title>' // 节点名字
children: [], // 子节点列表
} // 样例数据
const ExampleTreeRoot = {
name: "Top",
children: [{
name: "Level 1",
children: [{
name: "Level 1-1",
children: [],
},
{
name: "Level 1-2",
children: []
}
],
},
{
name: "Level 2",
children: [{
name: "Level 2-1",
children: [],
},
{
name: "Level 2-2",
children: []
}
]
}
]
}
// 按广度优先遍历返回的结果: [
"Top",
"Level 1",
"Level 2",
"Level 1-1",
"Level 1-2",
"Level 2-1",
"Level 2-2"
]

我一看,这不就是个最基础的层序遍历吗,你就拿这个考验干部,大家可以结合上面的讲解思路自己做做此题,答案如下:

const levelTraverse = (root) => {
if (root === null) return;
const queue = [];
const ans = [];
queue.push(root);
while (queue.length > 0) {
const node = queue.shift();
ans.push(node.name);
queue.push(...node.children);
};
return ans;
}

我就跟他说,你这考的也太简单了,不如再做个变体,我希望层序遍历输出的同时,同层的元素能装在同一个数组里:

大家可以先自己尝试下怎么写,其实思路还是一样,只是在遍历某一层时,我们需要额外再定一个levelArr来装这一层的数组,代码如下:

const levelTraverse = (root) => {
if (root === null) return;
const queue = [];
const ans = [];
queue.push(root);
while (queue.length > 0) {
// 创建每一层的数组
const levelArr = [];
// 获取每一层的长度
const len = queue.length;
// 每一层的操作单独再交给一个for来处理
for (let i = 0; i < len; i++) {
const node = queue.shift();
levelArr.push(node.name);
queue.push(...node.children);
}
ans.push(levelArr);
};
return ans;
}

这种做法跟之前有什么区别呢?之前我们始终维护一个queue,然后不断的入队列----判断队列长度----出队列----记录值----将子加入队列。但是这个过程很明显不能让每一层的元素在同一个数组,因此我们每次处理某一层之前,先创建一个levelArr,之后我们肯定得知道当前层有几个数组,因此额外需要遍历一次,依次将当前层元素的值加入到levelArr,直到这一层遍历完成,我们值记录也完成了,同时这一层节点的子节点也被加入到queue了。

简单点来理解就是,我们通过queue控制还能不能遍历,通过内部的for来控制每一层具体的遍历操作,思维转换也比较简单,将原先while的处理逻辑丢到for里面即可,而需要做几次呢?我们可以通过queue的长度来决定,你要相信,当进入到下一轮queue时,queue一定只包含了某一层的元素。

虽然这里不是递归,但是while的迭代本质上跟递归没什么差异,上篇文章我教大家将while改成写递归也是这个目的,你只用关注当前需要做什么,后续的迭代(递归)会帮你做相同的事,大概就是这么个意思了。

那么在讲解完这个之后,我相信大家也能轻易拿下 leetcode 102. 二叉树的层序遍历,这个题难度还是中等,但是我相信对于现在的你而言非常简单,当然,我还是直接套模板给大家提供一个思路:

var levelOrder = function (root) {
// 这里是题目特性,它希望空时返回空数组
if (root === null) return [];
const ans = [];
const queue = [];
queue.push(root);
while (queue.length > 0) {
const len = queue.length;
const levelArr = [];
for (let i = 0; i < len; i++) {
const node = queue.shift();
levelArr.push(node.val);
if (node.left) {
queue.push(node.left);
};
if (node.right) {
queue.push(node.right);
};
}
ans.push(levelArr);
}
return ans;
};

伍 ❀ 总

OK,那么到这里,我们花了两个篇幅分别解释了二叉树的深度优先以及广度优先的模板含义,我相信在理解二叉树不同遍历方式到底是如何运转之后,对于模板的理解也会更上一个层次。也希望这两篇文章,在之后的二叉树算法中能为大家带来一定帮助,那么到这里本文结束。

五三想休息,今天还学习,图解二叉树的层序遍历BFS(广度优先)模板,附面试题题解的更多相关文章

  1. 二叉树的层序遍历 BFS

    二叉树的层序遍历,或者说是宽度优先便利,是经常考察的内容. 问题一:层序遍历二叉树并输出,直接输出结果即可,输出格式为一行. #include <iostream> #include &l ...

  2. Leetcode 102. Binary Tree Level Order Traversal(二叉树的层序遍历)

    Given a binary tree, return the level order traversal of its nodes' values. (ie, from left to right, ...

  3. 剑指offer 二叉树的层序遍历

    剑指offer 牛客网 二叉树的层序遍历 # -*- coding: utf-8 -*- """ Created on Tue Apr 9 09:33:16 2019 @ ...

  4. leetcode之二叉树的层序遍历

    1.题目描述 2.题目分析 二叉树的层序遍历主要算法思想是使用 队列这一数据结构实现,这个数据结构多应用在和 图相关的算法.例如图的广度优先遍历就可以使用队列的方法实现.本题的关键在于如何识别出一层已 ...

  5. LeetCode 102. 二叉树的层序遍历 | Python

    102. 二叉树的层序遍历 题目来源:https://leetcode-cn.com/problems/binary-tree-level-order-traversal 题目 给你一个二叉树,请你返 ...

  6. 刷题-力扣-107. 二叉树的层序遍历 II

    107. 二叉树的层序遍历 II 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/binary-tree-level-order-tr ...

  7. 二叉树的深度优先遍历与广度优先遍历 [ C++ 实现 ]

    深度优先搜索算法(Depth First Search),是搜索算法的一种.是沿着树的深度遍历树的节点,尽可能深的搜索树的分支. 当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点 ...

  8. 【LeetCode】102. Binary Tree Level Order Traversal 二叉树的层序遍历 (Python&C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 BFS DFS 日期 题目地址:https://lee ...

  9. leetcode题解:Tree Level Order Traversal II (二叉树的层序遍历 2)

    题目: Given a binary tree, return the bottom-up level order traversal of its nodes' values. (ie, from ...

随机推荐

  1. Mysql的索引及优化

    一:四种存储引擎: mysql使用 show engines查询其存储引擎: 功  能 MYISAM Memory InnoDB Archive 存储限制 256TB RAM 64TB None 支持 ...

  2. Redis的安装与启动(一)

    Redis是c语言开发的. 安装redis需要c语言的编译环境.如果没有gcc需要在线安装.yum install gcc-c++ 安装步骤: 第一步:redis的源码包上传到linux系统.--源码 ...

  3. SpringBoot集成ArtemisMQ,设置动态消息类型

    SpringBoot项目集成ArtemisMQ,那么想动态的更换消息类型,怎么办呢? 通过设置org.springframework.jms.support.destination.JmsDestin ...

  4. 重载(Overload)和重写(Override)的区别。重载的 方法能否根据返回类型进行区分?

    方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性.重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同.参数个数不同或者二者都不同)则视 ...

  5. 名词解析-SOA

    什么是SOA SOA全英文是Service-Oriented Architecture,中文意思是中文面向服务编程,是一种思想,一种方法论,一种分布式的服务架构 SOA的作用场景 SOA解决多服务凌乱 ...

  6. MyBatis Plus 2.3 个人笔记-03-Active Record

    AR 语法糖  是一种领域模型模式,特点就是一个模型类对应关系型数据库中的一个表,而模型类的一个实例对应表中的一条记录 实现AR [在代码生成器中可以添加配置] import com.baomidou ...

  7. Effective Java —— try-with-resources 优先于 try-finally

    本文参考 本篇文章参考自<Effective Java>第三版第九条"Prefer try-with-resources to try-finally" The cod ...

  8. .c文件和.h文件的关系

    参考:12 另一篇:c源文件中为什么要包含自己对应的头文件 问题 在进行C语言文件移植时,遇到 "通常是每个.c文件对应一个.h文件",之前了解过.h文件是头文件,用来引用其他文件 ...

  9. PWA 时代的移动端图片优化新思路

    图片通常是移动端网页中最占流量的资源.在很多类型的网站中(比如电商.社区),图片占据了网页的大部分空间.优化图片的质量和加载速度成为提高用户体验的重要途径. 传统方案的弊端 目前有一些被广泛应用的方案 ...

  10. try、catch、finally、return的执行顺序

    1. 不管有没有异常,finally里面的语句都会执行 2. 当try和catch中有返回语句时,finally里面的语句还是会执行 3. 如果finally里面没有return语句,try和catc ...