壹 ❀ 引

我在从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. synchronized 关键字的用法?

    synchronized 关键字可以将对象或者方法标记为同步,以实现对对象和方法的互 斥访问,可以用 synchronized(对象) { - }定义同步代码块,或者在声明方法时 将 synchron ...

  2. vue单文件组件data选项的函数体获取vue实例对象

    因配置的关系,导致 vue的data选项中存在事件.而事件无法获取 vue 的实例对象:项目是单文件形式的,以下代码只是例子 new Vue({ el:..., data:{ a: { onevent ...

  3. java-面向对象相关

    public class DemoMethodOverload { public static void main(String[] args) { int[] array = new int[]{1 ...

  4. 学习ELK日志平台(四)

    一:需求及基础: 场景: 1.开发人员不能登录线上服务器查看详细日志 2.各个系统都有日志,日志数据分散难以查找 3.日志数据量大,查询速度慢,或者数据不够实时 4.一个调用会涉及到多个系统,难以在这 ...

  5. OpenCV+QT5在Window下的环境配置记录

    在安装OpenCV时最需要注意的是,OpenCV库,也就是我们需要的dll和动态库需要我们使用CMake来生成. 虽然在官网上下载得到的文件中已经包含了库文件和.h等头文件,但是在具体开发中编译器编译 ...

  6. .NET Best Practices: Architecture & Design Patterns (5 Days Training)

    .NET Best Practices: Architecture & Design Patterns (5 Days Training) .NET最佳实践:架构及设计模式 5天培训课程 课程 ...

  7. C#ASP.NET网站开发步骤

    1. 创建项目ASP.NET Web 应用程序. 2. 选择"Web 窗体"模板,然后单击 "确定" 按钮创建项目. 3. 在解决方案资源管理器中,右键添加we ...

  8. Java/C++实现观察者模式--股票价格

    当股票的价格上涨或下降5%时,会通知持有该股票的股民,当股民听到价格上涨的消息时会买股票,当价格下降时会大哭一场. 类图: Java代码: public class Investor implemen ...

  9. XUtils 开发框架

    xUtils简介 xUtils 包含了很多实用的android工具. xUtils 最初源于Afinal框架,进行了大量重构,使得xUtils支持大文件上传,更全面的http请求协议支持(10种谓词) ...

  10. es6语法之模版字符串

    //es6语法之`${}`以及vue语法 `${ }` (模版字符串)const name = '小缘' const age = 14 console.info(`大家好,我叫${name},今年${ ...