LC 102 二叉树层序遍历:我当初硬写 DFS 卡了半小时,才发现 BFS 才是正解

69 阅读5分钟

刚刷二叉树那会,我总觉得递归是万能的。什么前中后序遍历,咔咔几行递归就写完了。直到遇到层序遍历这题,我头铁非要用 DFS 写,结果光分层就卡了我半小时。写出来的代码又丑又长,自己都看不下去。后来才知道,层序遍历天生就是为 BFS 准备的,用队列写出来的代码,干净得不像话。

题目说啥

给你一棵二叉树的根节点,让你一层一层从左到右输出节点值。每一层的节点单独放一个数组里,最后返回一个二维数组。比如示例里的树,输出就是[[3],[9,20],[15,7]]

我一开始的错误尝试

我当时真的一根筋,满脑子都是递归。思路是这样的:递归的时候记录当前的深度,然后把节点值 push 到对应深度的数组里。根节点深度是 0,就放到result[0],它的子节点深度是 1,放到result[1],以此类推。

其实这个方法是能跑通的,我也写出来了。但总觉得哪里不对,层序遍历怎么能用深度优先呢?这不是本末倒置吗?而且遇到后面的变形题,比如锯齿形遍历、右视图,用 DFS 写起来就特别别扭。

直到我搞懂了 BFS 和队列

说穿了其实特别简单。你想啊,层序遍历是一层一层走的,先进先出,这不就是队列的特性吗?

核心思路就一句话:用队列存待处理的节点,每次处理完整的一层,再处理下一层。

那怎么知道什么时候一层处理完了?这就是这道题最关键的一步,也是我当初卡了最久的地方:在开始处理每一层之前,先记录当前队列的长度。

这个长度,就是当前层的节点总数。然后我们循环这么多次,把这些节点都取出来处理,再把它们的左右子节点放进队列。等这个循环结束,当前层就处理完了,把这一层的结果放进最终数组就行。真的就这么简单。

我拿示例 1 的树给你走一遍:

  1. 一开始队列里只有根节点 3,队列长度是 1
  2. 循环 1 次,取出 3,放进当前层数组,把 9 和 20 放进队列
  3. 第一层处理完,把[3]放进结果
  4. 现在队列里是 [9,20],长度是 2
  5. 循环 2 次,依次取出 9 和 20,放进当前层数组,把 15 和 7 放进队列
  6. 第二层处理完,把[9,20]放进结果
  7. 现在队列里是 [15,7],长度是 2
  8. 循环 2 次,取出 15 和 7,放进当前层数组
  9. 第三层处理完,把[15,7]放进结果
  10. 队列空了,遍历结束

image.png

代码实现(BFS 推荐写法)

var levelOrder = function(root) {
    // 边界处理:空树直接返回空数组
    if (!root) return []
    
    const result = []
    // 初始化队列,先把根节点放进去
    const queue = [root]
    
    while (queue.length) {
        // 最关键的一步:记录当前层的节点数
        // 别问我为什么知道不能在循环里用queue.length
        // 我当初在这里debug了20分钟,队列一边出一边进,永远循环不完
        const levelSize = queue.length
        const currentLevel = []
        
        for (let i = 0; i < levelSize; i++) {
            // 取出队首节点
            const node = queue.shift()
            // 把当前节点的值放进当前层的数组
            currentLevel.push(node.val)
            
            // 左子节点存在就放进队列
            if (node.left) queue.push(node.left)
            // 右子节点存在就放进队列
            if (node.right) queue.push(node.right)
        }
        
        // 当前层处理完了,放进结果数组
        result.push(currentLevel)
    }
    
    return result
};

补充:DFS 写法(不推荐但可以了解)

当然,如果你非要用 DFS 写,也不是不行。代码其实也不长,就是靠递归深度来分层:

var levelOrder = function(root) {
    const result = []
    
    const dfs = (node, depth) => {
        if (!node) return
        
        // 如果当前深度的数组还不存在,就创建一个
        if (!result[depth]) {
            result[depth] = []
        }
        
        // 把当前节点值放进对应深度的数组
        result[depth].push(node.val)
        
        // 递归处理左右子节点,深度+1
        dfs(node.left, depth + 1)
        dfs(node.right, depth + 1)
    }
    
    // 从根节点开始,深度是0
    dfs(root, 0)
    
    return result
};

但我还是强烈建议你掌握 BFS 的方法。因为这是一个万能模板,后面几乎所有层序相关的题,都是在这个模板上改。比如 103 锯齿形层序遍历,你只需要在 push 当前层的时候,判断一下深度是奇数还是偶数,奇数就反转一下数组就行。比如 199 二叉树的右视图,你只需要每次取当前层的最后一个节点就行。学会这个模板,你能一口气刷完十几道树的中等题,血赚。

复杂度分析

两种方法的时间复杂度都是 O (n),每个节点都只会被访问一次。空间复杂度也都是 O (n),BFS 的队列最多存一层的节点,最坏情况是满二叉树的最后一层,有 n/2 个节点。DFS 的递归栈最坏情况是树退化成链表,深度是 n。

最后说两句

这道题是 BFS 的入门必刷题,没有之一。我当初踩的最大的坑,就是不知道要先记录当前层的节点数。后来才明白,原来只要这一个小技巧,能解决 90% 的层序遍历问题。我现在刷到层序相关的题,上来先写这个模板,然后改改业务逻辑就过了,特别爽。

你当初刷这道题的时候,是先想到 DFS 还是 BFS?有没有在记录当前层长度这里踩过坑?或者你有什么更巧妙的写法?评论区聊聊,我每条都会看的。如果觉得这篇题解对你有帮助,点个赞让更多人看到呀~