二叉树的层序遍历:4道例题讲解

例一

429. N 叉树的层序遍历 - 力扣(LeetCode)先来看这道题:

为什么需要引入队列

因为层序遍历的本质是按 "层" 的顺序处理节点 :先处理第 1 层(根节点),再处理第 2 层(根的所有子节点),接着处理第 3 层(第 2 层每个节点的子节点)......

而队列的特性是 "先进先出",刚好能匹配这种 "按顺序处理、按顺序传递下一层节点" 的逻辑:

  1. 先把当前层的所有节点依次加入队列;
  2. 处理当前层时,依次从队列头部取出节点,同时把该节点的所有子节点加入队列尾部;
  3. 那么当前层的节点全部出队之后,队列里剩下的就是下一层的所有节点

引入变量来记录当前队列中元素的个数

为什么必须引入这个变量

由于队列本身是 "连续的节点"。

那么我们在处理的过程中很可能出现下面这种情况:
当前层节点还没处理完时,下一层节点已经在往队列里加了。

如果我们不记录 "当前层有多少个节点",那么我们并不知道 "处理到哪个节点时,当前层就结束了"。

反例

以示例一为例,我们假设不记录队列长度,直接循环取队列元素:

初始队列:[1] → 取 1,加子节点 3、2、4 → 队列变成 [3,2,4]

接着取 3,加子节点 5、6 → 队列变成 [2,4,5,6]

再取 2 → 队列变成 [4,5,6]......

那么我们不难发现:当前层(第 2 层)的 3、2、4 还没处理完,下一层(第 3 层)的 5、6 已经入队了。

所以我们根本无法区分 "哪些节点属于第 2 层,哪些属于第 3 层",最终输出的结果会是混乱的 [1,3,2,5,6,4],完全不是层序遍历要求的 "逐层分组"。

这个变量的作用

这个变量的作用是"给当前层画一个'边界"------

它告诉我们:"接下来要处理的count个节点,全是当前层的;处理完这count个,当前层就结束了,那么剩下的队列元素都是下一层的"。

具体流程

  1. 初始队列:[1] ,并记录 count=队列长度=1(这是第 1 层的节点数);
  2. 循环count次(即处理 1 个节点):
    • 1 ,并向最终的List<List<Integer>>中加入结果 [1]
    • 加子节点 3、2、4 ,队列变成 [3,2,4]
  3. 循环结束,所以第 1 层处理完成;
  4. 记录新的count=队列长度=3(这是第 2 层的节点数);
  5. 循环count次(处理 3 个节点):
    • 3 , 加入结果 [3];加子节点 5、6 → 队列变成 [2,4,5,6]
    • 2 , 加入结果 [2];无孩子子节点 → 队列变成 [4,5,6]
    • 4 , 加入结果 [4];无孩子节点 → 队列变成 [5,6]
  6. 循环结束 → 第 2 层处理完成(并向最终的List<List<Integer>>中加入结果[3,2,4]);
  7. 记录新的 count=队列长度=2(这是第 3 层的节点数);
  8. 循环count次 ,处理 5、6 并向最终的List<List<Integer>>中加入结果[5,6]

完整的解题代码

java 复制代码
/*
// Definition for a Node.
class Node {
    public int val;
    public List<Node> children;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val, List<Node> _children) {
        val = _val;
        children = _children;
    }
};
*/

class Solution {
    public List<List<Integer>> levelOrder(Node root) {
        List<List<Integer>> ret=new ArrayList<>();
        if(root==null){
            return ret;
        }
        Queue<Node> q=new LinkedList<>();
        q.add(root);

        while(!q.isEmpty()){
            int count=q.size();
            List<Integer> tmp=new ArrayList<>();

            for(int i=0;i<count;i++){
                Node t=q.poll();
                tmp.add(t.val);
                for(Node child : t.children){
                    if(child!=null){
                        q.add(child);
                    }
                }
            }
            ret.add(tmp); 
        }

        return ret;
    }
}

例二

sl103. 二叉树的锯齿形层序遍历 - 力扣(LeetCode)

这道题和上一道题的区别在于:

这题的第偶数层的节点需要逆序存储起来,于是我们可以引入一个flag变量来进行标记当前是偶数层还是奇数层,如果是偶数层,就把该层的结果逆序存储起来。

完整的解题代码

java 复制代码
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
        List<List<Integer>> ret=new ArrayList<>();
        if(root==null){
            return ret;
        }
        int flag=1;
        Queue<TreeNode> q=new LinkedList<>();
        q.add(root);
        while(!q.isEmpty()){
            List<Integer> tmp=new ArrayList<>();
            int size=q.size();
            for(int i=0;i<size;i++){
                TreeNode out=q.poll();
                tmp.add(out.val);
                if(out.left!=null){
                    q.add(out.left);
                }
                if(out.right!=null){
                    q.add(out.right);
                }
            }
            if(flag%2==0){
                Collections.reverse(tmp);
            }
            flag++;
            ret.add(tmp);
        } 
        return ret;
    }
}

例三

662. 二叉树最大宽度 - 力扣(LeetCode)

本题要注意的是当一个节点没有左(右)孩子的时候,我们应该要把它给补齐。

我们本题可以借用数组来实现,这个数组中存储的每个元素是一个Pair<TreeNode,Integer>.

如上图所示,二叉树中的一个节点的编号为x,那么他的左右孩子的编号分别为2x和2x+1.

于是我们就可以借助这个性质,把当前节点以及当前节点的编号都存放在这个Pair中,然后再把这个Pair放到数组当中:

java 复制代码
List<Pair<TreeNode,Integer>> q=new ArrayList<>();
q.add(new Pair<TreeNode,Integer>(root,1));

细节处理

我们借用数组实现队列功能时,为了保持"先进先出"的特性,如果直接在原数组中删除首元素会导致较高时间复杂度。
更高效的做法是:将下一层元素暂存到临时数组中,最后用该临时数组替换原队列q。

易错点

这里我们不应该直接使用t.left,应当像下面这样的方式进行处理:

java 复制代码
                TreeNode node=t.getKey();
                int index=t.getValue();
                if(node.left!=null){
                    tmp.add( new Pair<TreeNode,Integer> (node.left,index*2));
                }

完整的解题代码

java 复制代码
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int widthOfBinaryTree(TreeNode root) {
        List<Pair<TreeNode,Integer>> q=new ArrayList<>();
        q.add(new Pair<TreeNode,Integer>(root,1));
        int ret=0;

        while(!q.isEmpty()){
            //已经进入当前层了,可以把当前层的结果先记录下来
            Pair<TreeNode,Integer> first=q.get(0);
            Pair<TreeNode,Integer> last=q.get(q.size()-1);
            ret=Math.max(ret,last.getValue()-first.getValue()+1);

            //接下来让下一层的孩子入队就行,由于本层的节点都在q这个队列里面存储,所以接下来我们只需要遍历这个q队列即可:
            List<Pair<TreeNode,Integer>> tmp=new ArrayList<>();

            for(Pair<TreeNode,Integer> t : q){
                TreeNode node=t.getKey();
                int index=t.getValue();
                if(node.left!=null){
                    tmp.add( new Pair<TreeNode,Integer> (node.left,index*2));
                }
                if(node.right!=null){
                    tmp.add( new Pair<TreeNode,Integer> (node.right,index*2+1));
                }
            }
            //上面的for循环遍历之后,tmp里面存储的就是下一层的节点元素,那么此时需要把tmp复制给p,来进行下一次的计算宽度的操作。
            q=tmp;
        }

        return ret;
    }
}

例四

LCR 044. 在每个树行中找最大值 - 力扣(LeetCode)

本题的解法只需在层序遍历的基础之上,引入一个变量来记录当前层的最大值即可。

完整的解题代码

java 复制代码
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> largestValues(TreeNode root) {
        List<Integer> ret=new ArrayList<>();
        if(root==null){
            return ret;
        }
        Queue<TreeNode> q=new LinkedList<>();
        q.add(root);
        while(!q.isEmpty()){
            int result=Integer.MIN_VALUE; 
            int size=q.size();
            for(int i=0;i<size;i++){
                TreeNode t=q.poll();
                result=Math.max(result,t.val);
                if(t.left!=null){
                    q.add(t.left);
                }
                if(t.right!=null){
                    q.add(t.right);
                }
            }
            ret.add(result); 
        }
        return ret;
    }
}

易错点

记录每层的最大值的result变量应该放在while循环内部,这样才能确保每次进行找最大值之前的result都是最小值。

如果我们把result设置为了全局变量,那么就会跨层复用了上一层的最大值,导致后一层的计算 "起点不对"------ 每一层的最大值应该只基于当前层的节点,而不是基于之前所有层的最大值。

结语

这四道题看似场景各异,但他们的核心思想是高度统一的

这些算法都基于队列的"先进先出"特性来实现层序遍历:首先利用队列对节点进行分层存储,然后针对每一层节点执行特定业务逻辑(如分组存储、奇偶层逆序、计算节点编号差或求取层内最大值等)。

总结来说,队列是层序遍历的核心基础,而各类业务逻辑只是在这个"逐层处理节点"的通用框架上进行灵活拓展。

相关推荐
杰瑞不懂代码2 小时前
【公式推导】AMP算法比BP算法强在哪(二)
python·算法·机器学习·概率论
野蛮人6号2 小时前
力扣热题100道之45跳跃游戏2
算法·leetcode·游戏
唐僧洗头爱飘柔95272 小时前
【区块链技术(05)】区块链核心技术:哈希算法再区块链中的应用;区块哈希与默克尔树;公开密钥算法、编码和解码算法(BASE58、BASE64)
算法·区块链·哈希算法·base64·默克尔树·区块哈希·公私钥算法
不能只会打代码2 小时前
力扣--3578. 统计极差最大为 K 的分割方式数(Java实现,代码注释及题目分析讲解)
算法·leetcode·动态规划·滑动窗口
小尧嵌入式2 小时前
QT软件开发知识流程及秒表计时器开发
开发语言·c++·qt·算法
踢球的打工仔2 小时前
前端html(3)
前端·算法·html
程序员-King.2 小时前
day114—同向双指针(数组)—统计得分小于K的子数组数目(LeetCode-2302)
算法·leetcode·双指针
智算菩萨2 小时前
深度学习在教育数据挖掘(EDM)中的方法体系:从任务建模到算法范式的理论梳理与总结
深度学习·算法·数据挖掘
_OP_CHEN2 小时前
【算法基础篇】(二十七)从记忆化搜索到动态规划:保姆级入门指南,带你吃透 DP 核心思想!
算法·蓝桥杯·动态规划·记忆化搜索·算法竞赛·acm/icpc