壹 ❀ 引

我在从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. Dubbo 服务降级,失败重试怎么做?

    可以通过 dubbo:reference 中设置 mock="return null".mock 的值也可以修 改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名 ...

  2. JS字符串格式化

    //字符串格式化String.prototype.format = function () { var values = arguments; return this.replace(/\{(\d+) ...

  3. 给定一个奇数n,比如n=3,生成1到n平方的数,如1到9,填入九宫格,使得横竖斜的和都相等。

    对于N阶幻方,从1开始把数字从小到大按以下规则依次写入: 一.在第一行中间一列写入1 二.依次向右上方写入2.3.4...... 三.如果某数字写在了表格的某个方向外面,那就把这个数字向相反方向移动N ...

  4. 学习Solr(二)

    一.Solr概述 1.什么是Solr Solr 是Apache下的一个顶级开源项目,采用Java开发,它是基于Lucene的全文搜索服务器.Solr提供了比Lucene更为丰富的查询语言,同时实现了可 ...

  5. c源文件中为什么要包含自己对应的头文件

    另一篇:.c文件和.h文件的关系 引言: 我们经常在c工程中发现,源文件中要包含自己的头文件.一直以来,都不知道为什么这样做.现在,我知道了. 以前的认知: 我认为,.c文件没有必要包含自己的.h文件 ...

  6. 罗振宇2022"时间的朋友"跨年演讲

    罗振宇2022"时间的朋友"跨年演讲 行就行,不行我再想想办法. 原来,还能这么干! 堆资源不是解决问题的唯一道路,还是那句话:"处于困境中的人往往只关注自己的问题.而解 ...

  7. 让弹幕给 PPD 生个孩子

    怎样才能跑起来我写的弹幕程序 资源下载 申请野狗后端云账号注册 创建应用: 复制appId到index.html的 var ref = new Wilddog("https://<ap ...

  8. 用jq实现移动端滑动轮播以及定时轮播效果

    Html的代码: <div class="carousel_img"> <div class="car_img" style="ba ...

  9. HTML5 Audio & Video 属性解析

    一.HTML 音频/视频 方法 play() play() 方法开始播放当前的音频或视频. var myVideo=document.getElementById("video1" ...

  10. vue引入swiper

    https://github.com/surmon-china/vue-awesome-swiper/blob/master/examples/03-pagination.vue https://su ...