力扣解题-103. 二叉树的锯齿形层序遍历

0 阅读7分钟

力扣解题-103. 二叉树的锯齿形层序遍历

给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

示例 1:

image.png

输入:root = [3,9,20,null,null,15,7]

输出:[[3],[20,9],[15,7]]

示例 2:

输入:root = [1]

输出:[[1]]

示例 3:

输入:root = []

输出:[]

提示:

树中节点数目在范围 [0, 2000] 内

-100 <= Node.val <= 100

Related Topics

树、广度优先搜索、二叉树


第一次解答

解题思路

核心方法:BFS层级遍历+方向标记反转法,基于标准层序遍历逻辑,通过布尔标记leftToRight控制每层遍历结果的顺序,偶数层(从0开始)正常存储,奇数层反转列表,实现“左→右、右→左”交替的锯齿形遍历,时间复杂度O(n)、空间复杂度O(n),是本题最直观、易实现的解法。

核心逻辑拆解

锯齿形层序遍历的核心是“标准层序遍历+层内顺序反转”,关键在于精准控制每层的顺序切换:

  1. 空树处理:若根节点root == null,直接返回空列表(无节点可遍历);
  2. 初始化
    • 结果列表res:存储各层节点值列表;
    • 队列queue:存储待遍历节点,先将根节点入队;
    • 方向标记leftToRight:初始为true(第一层从左到右);
  3. 层级遍历循环:队列非空时,持续处理每一层:
    • 创建层列表:为当前层新建空列表list,用于存储该层节点值;
    • 固定层大小:遍历前记录队列长度queue.size(),通过倒序循环(i>0)确保只处理当前层节点;
    • 处理当前层节点:逐个弹出队列节点,将节点值加入层列表,同时按“先左后右”顺序将子节点入队(保证层序遍历的基础顺序);
    • 调整层内顺序:若leftToRightfalse(需从右到左),反转当前层列表;
    • 更新结果与方向:将调整后的层列表加入结果列表,切换leftToRight标记(为下一层做准备);
  4. 返回结果:遍历完所有层后,返回存储锯齿形结果的列表。
具体步骤(以示例1 root=[3,9,20,null,null,15,7]为例)
遍历层级层大小原始层列表leftToRight是否反转最终层列表结果列表更新方向切换后
01[3]true[3][[3]]false
12[9,20]false[20,9][[3],[20,9]]true
22[15,7]true[15,7][[3],[20,9],[15,7]]false
关键细节说明
  • 方向切换时机:必须在整层处理完成后切换leftToRight,而非单个节点处理时切换,确保“层与层交替”而非“节点交替”;
  • 反转操作:使用Collections.reverse(list)实现列表反转,时间复杂度O(k)(k为当前层节点数),整体仍为O(n);
  • 冗余判断说明:代码中if(value!=null)是鲁棒性设计(题目中节点值非null),不影响核心逻辑;
  • 入队顺序:始终保持“先左后右”入队,保证原始层列表是标准层序顺序,仅通过反转调整最终输出。
性能说明
  • 时间复杂度:O(n)(每个节点仅入队/出队一次,反转操作总耗时为O(n),整体仍为线性复杂度);
  • 空间复杂度:O(n)(最坏情况队列存储一层所有节点,如完全二叉树最后一层有n/2个节点);
  • 优势:
    1. 基于经典层序遍历改造,逻辑直观,新手易理解和实现;
    2. 代码简洁,仅需添加方向标记和反转操作,拓展性强;
    3. 边界处理完善,兼容空树、单节点树等所有场景。
    public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
        if(root==null){
            return new ArrayList<>();
        }
        List<List<Integer>> res=new ArrayList<>();
        Queue<TreeNode> queue=new LinkedList<>();
        queue.offer(root);
        boolean leftToRight = true; // 标记当前层方向
        while(!queue.isEmpty()){
            List<Integer> list=new ArrayList<>();
            for(int i=queue.size();i>0;i--){
                TreeNode node = queue.poll();
                Integer value=node.val;
                if(value!=null){
                    list.add(value);
                }
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
            // 如果当前层是从右到左,反转 list
            if (!leftToRight) {
                Collections.reverse(list);
            }
            res.add(list);
            leftToRight = !leftToRight; // 切换方向(整层切换!)
        }
        return res;
    }

示例解答

解题思路

解法1:双端队列法(无需反转,效率更优)

核心方法:利用Deque(双端队列)的特性,根据当前层方向决定节点值从队列头部/尾部加入,避免列表反转操作,时间复杂度仍为O(n),但减少了反转的额外开销,是更高效的进阶解法。

代码实现
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }
    List<List<Integer>> res = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    boolean leftToRight = true; // 标记当前层方向
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        // 双端队列存储当前层节点值,灵活控制添加方向
        Deque<Integer> deque = new LinkedList<>();
        
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            // 左到右:从尾部添加;右到左:从头部添加
            if (leftToRight) {
                deque.offerLast(node.val);
            } else {
                deque.offerFirst(node.val);
            }
            // 子节点仍按先左后右入队
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
        // 双端队列转列表加入结果
        res.add(new ArrayList<>(deque));
        // 切换下一层方向
        leftToRight = !leftToRight;
    }
    return res;
}
核心逻辑说明
  1. 双端队列优势Deque支持从头部/尾部添加元素,无需先按顺序存储再反转;
  2. 方向控制添加
    • 左到右:deque.offerLast(node.val)(尾部添加,保持顺序);
    • 右到左:deque.offerFirst(node.val)(头部添加,自然形成逆序);
  3. 结果转换:将双端队列直接转为ArrayList,无需额外反转操作。
性能说明
  • 时间复杂度:O(n)(无反转操作,减少了每层的额外遍历);
  • 空间复杂度:O(n)(双端队列仅存储当前层节点值,空间开销与原解法一致);
  • 优势:
    1. 避免列表反转的额外开销,效率略高于原解法;
    2. 更贴合“锯齿形”的语义,直接控制元素添加顺序而非事后调整;
  • 劣势:需理解双端队列的使用,新手入门门槛略高。
解法2:DFS递归法(标记层级+方向)

核心方法:深度优先搜索,通过层级参数标记节点所属层级,结合层级奇偶性控制节点值的添加位置(偶数层尾部添加,奇数层头部添加),实现锯齿形遍历,时间复杂度O(n)、空间复杂度O(h)(h为树的高度)。

代码实现
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
    List<List<Integer>> res = new ArrayList<>();
    // 从根节点、层级0开始递归
    dfs(root, 0, res);
    return res;
}

private void dfs(TreeNode node, int level, List<List<Integer>> res) {
    if (node == null) {
        return;
    }
    // 首次访问该层级,初始化空列表
    if (level == res.size()) {
        res.add(new ArrayList<>());
    }
    List<Integer> levelList = res.get(level);
    // 偶数层(0、2、4...):尾部添加(左到右);奇数层:头部添加(右到左)
    if (level % 2 == 0) {
        levelList.add(node.val);
    } else {
        levelList.add(0, node.val);
    }
    // 递归遍历左右子树,层级+1
    dfs(node.left, level + 1, res);
    dfs(node.right, level + 1, res);
}
核心逻辑说明
  1. 层级判断level == res.size() 表示首次访问该层级,需初始化空列表;
  2. 方向控制
    • 偶数层级(0开始):add(node.val)(尾部添加,左到右);
    • 奇数层级:add(0, node.val)(头部添加,自然形成右到左);
  3. 遍历顺序:先左后右递归,保证同一层级内节点的基础顺序。
性能说明
  • 时间复杂度:O(n)(每个节点仅被访问一次);
  • 空间复杂度:O(h)(递归栈深度等于树的高度,平衡树为O(logn),斜树为O(n));
  • 优势:
    1. 无需使用队列,空间复杂度在平衡树场景下优于BFS;
    2. 代码简洁,直接通过层级奇偶性控制顺序;
  • 劣势:
    1. add(0, node.val) 操作时间复杂度为O(k)(k为当前层节点数),整体效率略低;
    2. 递归逻辑稍抽象,依赖层级标记映射到锯齿形顺序。

总结

  1. BFS反转法(第一次解答):O(n)时间+O(n)空间,逻辑直观、易实现,是新手入门的首选解法;
  2. 双端队列BFS法:O(n)时间+O(n)空间,无反转开销,效率更优,适合工程实践;
  3. DFS递归法:O(n)时间+O(h)空间,无需队列,平衡树场景下空间更优;
  4. 关键技巧
    • 核心思想:锯齿形遍历的本质是“层内顺序交替”,可通过“事后反转”或“实时控制添加顺序”实现;
    • 方向切换:必须按“整层”切换,而非单个节点;
    • 性能选择:优先选双端队列法(高效)或反转法(易理解),DFS仅作拓展思路。