力扣解题-103. 二叉树的锯齿形层序遍历
给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
示例 1:
输入: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),是本题最直观、易实现的解法。
核心逻辑拆解
锯齿形层序遍历的核心是“标准层序遍历+层内顺序反转”,关键在于精准控制每层的顺序切换:
- 空树处理:若根节点
root == null,直接返回空列表(无节点可遍历); - 初始化:
- 结果列表
res:存储各层节点值列表; - 队列
queue:存储待遍历节点,先将根节点入队; - 方向标记
leftToRight:初始为true(第一层从左到右);
- 结果列表
- 层级遍历循环:队列非空时,持续处理每一层:
- 创建层列表:为当前层新建空列表
list,用于存储该层节点值; - 固定层大小:遍历前记录队列长度
queue.size(),通过倒序循环(i>0)确保只处理当前层节点; - 处理当前层节点:逐个弹出队列节点,将节点值加入层列表,同时按“先左后右”顺序将子节点入队(保证层序遍历的基础顺序);
- 调整层内顺序:若
leftToRight为false(需从右到左),反转当前层列表; - 更新结果与方向:将调整后的层列表加入结果列表,切换
leftToRight标记(为下一层做准备);
- 创建层列表:为当前层新建空列表
- 返回结果:遍历完所有层后,返回存储锯齿形结果的列表。
具体步骤(以示例1 root=[3,9,20,null,null,15,7]为例)
| 遍历层级 | 层大小 | 原始层列表 | leftToRight | 是否反转 | 最终层列表 | 结果列表更新 | 方向切换后 |
|---|---|---|---|---|---|---|---|
| 0 | 1 | [3] | true | 否 | [3] | [[3]] | false |
| 1 | 2 | [9,20] | false | 是 | [20,9] | [[3],[20,9]] | true |
| 2 | 2 | [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个节点);
- 优势:
- 基于经典层序遍历改造,逻辑直观,新手易理解和实现;
- 代码简洁,仅需添加方向标记和反转操作,拓展性强;
- 边界处理完善,兼容空树、单节点树等所有场景。
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;
}
核心逻辑说明
- 双端队列优势:
Deque支持从头部/尾部添加元素,无需先按顺序存储再反转; - 方向控制添加:
- 左到右:
deque.offerLast(node.val)(尾部添加,保持顺序); - 右到左:
deque.offerFirst(node.val)(头部添加,自然形成逆序);
- 左到右:
- 结果转换:将双端队列直接转为
ArrayList,无需额外反转操作。
性能说明
- 时间复杂度:O(n)(无反转操作,减少了每层的额外遍历);
- 空间复杂度:O(n)(双端队列仅存储当前层节点值,空间开销与原解法一致);
- 优势:
- 避免列表反转的额外开销,效率略高于原解法;
- 更贴合“锯齿形”的语义,直接控制元素添加顺序而非事后调整;
- 劣势:需理解双端队列的使用,新手入门门槛略高。
解法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);
}
核心逻辑说明
- 层级判断:
level == res.size()表示首次访问该层级,需初始化空列表; - 方向控制:
- 偶数层级(0开始):
add(node.val)(尾部添加,左到右); - 奇数层级:
add(0, node.val)(头部添加,自然形成右到左);
- 偶数层级(0开始):
- 遍历顺序:先左后右递归,保证同一层级内节点的基础顺序。
性能说明
- 时间复杂度:O(n)(每个节点仅被访问一次);
- 空间复杂度:O(h)(递归栈深度等于树的高度,平衡树为O(logn),斜树为O(n));
- 优势:
- 无需使用队列,空间复杂度在平衡树场景下优于BFS;
- 代码简洁,直接通过层级奇偶性控制顺序;
- 劣势:
add(0, node.val)操作时间复杂度为O(k)(k为当前层节点数),整体效率略低;- 递归逻辑稍抽象,依赖层级标记映射到锯齿形顺序。
总结
- BFS反转法(第一次解答):O(n)时间+O(n)空间,逻辑直观、易实现,是新手入门的首选解法;
- 双端队列BFS法:O(n)时间+O(n)空间,无反转开销,效率更优,适合工程实践;
- DFS递归法:O(n)时间+O(h)空间,无需队列,平衡树场景下空间更优;
- 关键技巧:
- 核心思想:锯齿形遍历的本质是“层内顺序交替”,可通过“事后反转”或“实时控制添加顺序”实现;
- 方向切换:必须按“整层”切换,而非单个节点;
- 性能选择:优先选双端队列法(高效)或反转法(易理解),DFS仅作拓展思路。