【算法-LeetCode】102. 二叉树的层序遍历(二叉树;层序遍历;BFS;生成二叉树)

102. 二叉树的层序遍历 - 力扣(LeetCode)

发布:2021年7月30日16:09:20

问题描述及示例

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

示例:
二叉树:[3,9,20,null,null,15,7],
在这里插入图片描述
返回其层序遍历结果:
[
  [3],
  [9,20],
  [15,7]
]

我的题解

我的题解1(前序遍历树并用depth记录深度)

成功前的尝试

看到这道题目时,我脑子里首先联想到的就是之前学过的广度优先遍历(Breadth First Search,简称:BFS),其实这道题的题目的名字也说了这是层序遍历,其实也就是BFS的应用。我隐约记得BFS是在讲图的遍历那一部分的内容。当然二叉树也可以看做是一种特殊的图嘛,自然也能应用BFS。但是我有点记不大清相关的细节了。于是脑子里又马上蹦出了另一个想法。也就是用一个变量depth来存储当前的遍历节点所在层的深度,将深度相同的节点存入同一个数组,最后将各层获得的结果放入result中,并将result作为结果返回即可。由于这几天我刚好做过回溯相关的题目,于是脑子里又马上有了应用回溯的思想来维护depth的大概想法,于是我初步写出了下面这个程序:

// 因为结果错误,而且我也讲不清楚错误细节,所以就不多做注释了
var levelOrder = function (root) {
   let depth = -1;
   // level 是用来动态存储某一层的节点的
   let level = [];
   // result 是最终结果
   let result = [];
   preTraverse(root);
   return result;

   function preTraverse(node) {
     if (!!node) {
       depth++;
       // level.pop();
       // console.log('depth', depth);
       level.push(node.val);
       // console.log('level', level);
       result[depth] = level;
       // console.log('result', result);
       preTraverse(node.left);
       depth--;
       preTraverse(node.right);
       depth--;
     }
   }
};

想法固然是很美好,但是可惜运行出来的结果非常奇怪

(2) [Array(5), Array(5), -1: Array(5), -2: Array(5)]
	0: (5) [3, 9, 20, 15, 7]
	1: (5) [3, 9, 20, 15, 7]
	2: (5) [3, 9, 20, 15, 7]
	-1: (5) [3, 9, 20, 15, 7]
length: 3

初步判断是depth没有维护好,同时level也没有根据深度的变化而及时改变。于是我开始修改上述的代码。期间要用到代码调试,但是LeetCode的相关功能要付费,没办法,我只能想着能不能自己实现一个根据节点的数组序列来创建一颗树的函数了,写了一部分之后我突然想到也许早就有人写了相关的函数了,于是经过一番搜索,我找到了符合我的要求的建树函数,现在就可以在本地进行调试了。以后碰上二叉树的题目也不用在脑子里想象了(此处留下贫穷的泪水……)。主要参考以下博客:

更新:2021年7月30日01:34:37
参考:leetcode使用javascript从一个数组中创建一颗树_Niap的博客-CSDN博客

参考:LeetCode二叉树构造方法,通过一维数组直接构建完整的二叉树(按照LeetCode的格式)_USTC暖暖的博客-CSDN博客

参考:LeetCode 根据题目给定的输入生成树_两颗橘子树的博客-CSDN博客

感谢以上博主的分享。我是直接用的JavaScript实现的那个函数(上面第一个参考)。

现在有了本地调试,我开始逐步观察代码执行,期间花费了较多的精力,过程也不详述了。最终有了我下面的第一个成功通过提交的题解。

前序遍历树并用depth记录深度(成功通过提交)

这里的区别就在于我将depth由一个全局变量改为了先序遍历函数preTraverse()中的的一个参数,这样就可以将depthresult的值代入下一层递归且不用临时遍历level来动态存储某一层的节点值了(具体原理比较难以描述,有意研究者可以在开发者工具中逐行调试来观察结果)。总体思路还是和我一开始尝试的一样,详解可看下方注释。

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function (root) {
  // result存储最终结果,注意result是作为一个二维数组返回的
  let result = [];
  // depth初始化为0,防止result[0]的结果为undefined
  preTraverse(root, 0, result);
  return result;

  // 前序遍历函数,depth用于记录当前节点的深度变化,同时也作为result数组的下标
  // 也可将depth理解为当前节点node的深度,下面的res是形参,注意不要和上面的result搞混了
  function preTraverse(node, depth, res) {
    // 如果当前节点为空,则结束一层递归
    if (!node) {
      return;
    }
    // 因为depth也要作为res的下标,res的长度是根据depth值的增长而同步增加的,
    // 如果depth增长至超出了res的长度,则说明树的遍历进入了下一层了,
    // 此时res数组就要再填入一个空数组用来存储新的一层的节点值了
    if (depth >= res.length) {
      res.push([]);
    }
    // 注意res[depth]也是一个数组哦
    res[depth].push(node.val);
    // 递归遍历当前节点node的左右子节点,此前的操作其实都可以抽象为对当前节点的访问
    // 因为要往下遍历子节点了,所以要记得要将depth+1作为下一层遍历的depth值
    // 注意下面的res值,这里为什么不像之前做的题那样做一个深拷贝再传入呢?值得推敲
    preTraverse(node.left, depth + 1, res);
    preTraverse(node.right, depth + 1, res);
  }
};


提交记录
34 / 34 个通过测试用例
状态:通过
执行用时:68 ms, 在所有 JavaScript 提交中击败了97.22%的用户
内存消耗:39.6 MB, 在所有 JavaScript 提交中击败了37.74%的用户
时间:2021/07/30 16:13

可以看到这种解法的空间性能表现不怎么好,毕竟是用到了递归嘛。当然其实也有非递归方式的前序遍历,空间性能应该会好一点,但此处就先不做研究了。

更新:2021年7月30日17:40:05
在我写上面的depth解法的过程中,我发现该题的评论区102. 二叉树的层序遍历 - 力扣(LeetCode)有一位题友【@宝宝可乖了】的思路和我上面的一模一样,代码的结构自然也很相似,我觉得他的那种写法更加的简洁优雅,所以就将上面的代码修改了一小部分。感谢这位题友的评论分享。

【更新结束】

我的题解2(BFS层序遍历树)

更新:2021年10月18日17:47:23

这段时间也陆续做了好几道层序遍历(BFS)的题目,我觉得都可以用本篇博客中的这种思路,稍作转变即可,有些题就是在本题的基础上稍微改一两行代码就可以了。详情可以看下方【有关参考】部分的整理。

【更新结束】

在看上面说到的评论区时,我也看到有题友说上面那种解法属于凑巧型,是LeetCode的测试用例集有bug才刚好通过,我倒不这么认为,因为无论是层序遍历还是先序遍历,总归是要逐个访问每一个节点的。上面的解法中只是用先序遍历的手法访问了所有的节点,然后在访问每个节点时通过观察我们所维护的depth变量来达到分辨当前层的效果,并将同一层的节点值放入对应下标为depthresult数组中,最后的结果还是存储在全局变量result中的(当然这里的全局并不是我们平常接触的全局哈,而是相对内部函数preTraverse()来说的,这里是为了方便表达)。所以说这样的方法只是用先序遍历的手法达到我们想要的层序遍历的结果而已,也是合理的。

那么既然题目都说了这题目的名字都提示我们用层序遍历完成这道题目,那就研究一下层序遍历如何操作吧。

更新:2021年7月30日19:41:32

吃了个饭回来继续。首先,在二叉树的层序遍历中,我们需要一个辅助队列结构queue来存储当前遍历节点,队列结构先进先出的特点正好可以满足我们广度优先搜索要求(而在深度遍历中,我们一般使用具有先进后出特点的栈结构来做辅助)。详解请看下方注释。

注意,我们说queue是存储当前遍历节点,但是千万不要以为queue中只会存储下面代码中所说的当前层中的节点 ,因为for循环在遍历当前层的所有节点时。也会将被我们所弹出的队首节点的左右子节点(如果子节点不为空的话)压入queue,所以queue是不断变化的。这也是为什么for循环中的i的结束条件我不是写成i < queue.length,而是先将queue.length的值存起来,就是为了防止不断变化的queue.length不会影响我们对某一层节点的遍历。

其实一开始我没有在意for循环中的i的结束条件是否被写成i < queue.length会给程序带来什么影响,我会写成len = queue.length; i < len;只是因为我习惯这样写,因为我觉得这样的话,在判断结束条件时,就不用每次都重新计算一遍数组的length值了,多少会快一点。事实上,这两种写法在一般情况下没有什么太大的区别,但在这里就比较关键了。我是看到【微信公众号:代码随想录】的博主写的相关资料中的代码注释里特意提到了这个i值的结束条件的问题,我才仔细思考了一下,发现确实不能简单地写成i<queue.length

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function (root) {
  let result = [];
  // 初始化queue为辅助队列,通过数组的push和shift方法的配合,可以实现队列先进先出的特点
  let queue = [];
  // 先把根节点放入队列,否则后续的while循环将无法工作
  queue.push(root);
  // 如果该树是一颗空树,那么直接返回空结果,
  // 不要漏掉这一步判断,否则无法通过所有LeetCode测试用例
  if(root === null) {
    return result;
  }
  // 如果队列不为空,则说明还没有遍历完所有层,注意这个while循环是以层为单位迭代的
  while (!!queue.length) {
  	// level用于存储当前层的所有节点,所以每新进一层时都应该将其初始化为空数组
    let level = [];
    // 这个for循环用于遍历当前层的所有节点,注意下面我为什么不直接写成 i<queue.length
    for (let i = 0, len = queue.length; i < len; i++) {
      // 弹出队首节点,但注意不要马上丢弃它,因为后面还用得上
      let node = queue.shift();
      // 将刚才弹出的节点值存入level
      level.push(node.val);
      // 下面两个if用于判断当刚才弹出的节点的左(右)子节点不为空时,则将子节点加入队尾
      if (!!node.left) {
        queue.push(node.left);
      }
      if (!!node.right) {
        queue.push(node.right);
      }
    }
    // for循环结束则说明当前层的节点已经全部存入level中,已经获得了一个结果
    result.push(level);
  }
  // while循环结束说明树中的所有层都已经遍历完,将结果返回
  return result;
};

提交记录
34 / 34 个通过测试用例
状态:通过
执行用时:72 ms, 在所有 JavaScript 提交中击败了93.51%的用户
内存消耗:39.4 MB, 在所有 JavaScript 提交中击败了76.86%的用户
时间:2021/07/30 19:35

总的来说,就是让上面的while循环负责二叉树深度方向的遍历,而for循环则负责某一层中的广度方向的遍历。其中利用queue这个辅助队列先进先出的结构特点来确保逐层遍历的效果。

while和for的作用

官方题解

更新:2021年7月29日18:43:21

因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。

更新:2021年7月30日16:16:21

参考:二叉树的层序遍历 - 二叉树的层序遍历 - 力扣(LeetCode)

【更新结束】

有关参考

更新:2021年7月30日21:03:06
参考:【微信公众号:代码随想录 2020-09-25】二叉树:层序遍历登场!
参考:深度优先遍历(DFS)和广度优先遍历(BFS)_JeansPocket的博客-CSDN博客_广度优先遍历
参考:js二维数组定义和初始化的三种方法总结_javascript技巧_脚本之家


【我做的其他层序遍历题目】
更新:2021年10月18日17:51:40
参考:【算法-LeetCode】199. 二叉树的右视图(二叉树;层序遍历;BFS)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】103. 二叉树的锯齿形层序遍历(二叉树;层序遍历;BFS)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】429. N 叉树的层序遍历(N叉树;层序遍历;BFS)_赖念安的博客-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值