在二叉树的层序遍历系列题目中,103题的锯齿形遍历算是一个“变种”——它没有遵循常规的“从左到右”逐层遍历,而是要求层与层之间交替方向,先左后右、再右后左,以此循环。今天我们就来拆解这道题,从思路分析到代码实现,再到易错点提醒,帮你彻底吃透这道经典面试题。
一、题目解读
题目要求:给定二叉树的根节点 root,返回其节点值的锯齿形层序遍历。所谓锯齿形,就是 奇数层(从0开始计数)从左往右遍历,偶数层从右往左遍历(或反之,核心是层与层方向交替)。
举个简单例子:
若二叉树为:
3
/ \
9 20
/ \
15 7
其锯齿形层序遍历结果为:[[3], [20,9], [15,7]]
解释:第0层(根节点)从左到右 → [3];第1层从右到左 → [20,9];第2层从左到右 → [15,7],完美符合“交替方向”的要求。
二、核心解题思路
锯齿形遍历的本质,还是 层序遍历(广度优先搜索BFS),因为我们必须先遍历完当前层的所有节点,才能进入下一层,这是层序遍历的核心逻辑。而“交替方向”只是在每层遍历完成后,对当前层的节点值数组做一个“反转”操作即可。
具体思路拆解:
-
初始化一个队列(用于BFS存储当前层的节点),一个结果数组(存储最终的锯齿形遍历结果),以及一个层数标记(用于判断当前层是否需要反转)。
-
如果根节点为空,直接返回空数组(边界条件处理)。
-
将根节点入队,然后进入循环:循环条件是队列不为空(还有节点未遍历)。
-
记录当前层的节点数量(levelSize),初始化一个临时数组(levelValues),用于存储当前层的节点值。
-
遍历当前层的所有节点:依次出队,将节点值存入临时数组,同时将该节点的左右子节点(如果存在)入队(注意:入队顺序始终是左→右,因为反转是在遍历完当前层后进行的,不影响子节点的入队顺序)。
-
判断当前层数:如果是奇数层(level % 2 === 1),将临时数组反转;如果是偶数层,保持不变。
-
将临时数组存入结果数组,层数加1,进入下一层循环。
-
循环结束后,返回结果数组。
关键亮点:无需改变节点的入队顺序,只需在每层遍历结束后,根据层数决定是否反转当前层的节点值数组,逻辑简单且高效。
三、完整代码实现(TypeScript)
先定义二叉树节点类(题目已给出,此处复用),再实现锯齿形层序遍历函数:
// 二叉树节点类定义
class TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val === undefined ? 0 : val)
this.left = (left === undefined ? null : left)
this.right = (right === undefined ? null : right)
}
}
// 锯齿形层序遍历函数
function zigzagLevelOrder(root: TreeNode | null): number[][] {
// 边界条件:根节点为空,返回空数组
if (!root) {
return [];
}
// 队列:存储当前层的节点,初始化存入根节点
const queue = [root];
// 结果数组:存储最终的锯齿形遍历结果
const result: number[][] = [];
// 层数标记:从0开始计数,用于判断是否需要反转
let level = 0;
// BFS循环:队列不为空则继续遍历
while (queue.length) {
// 记录当前层的节点数量,避免后续入队影响遍历次数
const levelSize = queue.length;
// 临时数组:存储当前层的节点值
const levelValues: number[] = [];
// 遍历当前层的所有节点
for (let i = 0; i < levelSize; i++) {
// 出队:取出当前层的第一个节点(队列是先进先出FIFO)
const node = queue.shift()!;
// 将当前节点值存入临时数组
levelValues.push(node.val);
// 左子节点存在则入队
if (node.left) {
queue.push(node.left);
}
// 右子节点存在则入队
if (node.right) {
queue.push(node.right);
}
}
// 关键:奇数层反转,偶数层不反转
if (level % 2 === 1) {
levelValues.reverse();
}
// 将当前层的节点值数组存入结果
result.push(levelValues);
// 层数加1,进入下一层
level++;
}
return result;
};
四、代码细节解析
1. 边界条件处理
if (!root) { return []; }:当根节点为空时,直接返回空数组,避免后续队列操作报错,这是二叉树题目中最基础也最容易忽略的一步。
2. 队列的作用
队列(queue)用于存储当前层的所有节点,遵循“先进先出(FIFO)”原则,保证层序遍历的顺序。每次循环开始时,用levelSize记录当前层的节点数量,确保我们只遍历当前层的节点,不会提前遍历到下一层的节点。
3. 反转逻辑的时机
反转操作是在遍历完当前层所有节点后进行的,而不是在遍历节点的过程中。如果在遍历过程中改变入队顺序,会导致下一层的遍历顺序出错,反而增加复杂度。
4. 类型标注(TypeScript)
函数参数root标注为TreeNode | null,避免传入非节点类型;result标注为number[][],明确返回值是二维数组(每一层一个数组);levelValues标注为number[],确保存入的是节点值(数字类型),提升代码可读性和健壮性。
五、易错点提醒
-
混淆层数计数:题目中的“锯齿形”是层与层交替,建议层数从0开始计数(如示例中第0层不反转,第1层反转),如果从1开始计数,需要调整判断条件(level % 2 === 0时反转),容易出错。
-
忘记记录levelSize:如果不记录当前层的节点数量,直接循环queue.length,会因为遍历过程中不断入队(左右子节点)导致循环次数异常,遍历到下一层的节点。
-
反转时机错误:在遍历节点时反转,会导致当前层节点值顺序混乱,正确的时机是遍历完当前层所有节点后,再根据层数判断是否反转。
-
空节点处理:入队前必须判断节点的左右子节点是否存在(if (node.left)、if (node.right)),否则会将null入队,后续shift()后访问val会报错。
六、复杂度分析
-
时间复杂度:O(n),其中n是二叉树的节点总数。每个节点都会入队一次、出队一次,反转操作的时间复杂度是O(k)(k是当前层的节点数),所有层的反转操作总时间是O(n),因此整体时间复杂度为O(n)。
-
空间复杂度:O(n),队列最多存储二叉树的一层节点(最坏情况是满二叉树的最后一层,节点数为n/2),结果数组存储所有节点值,空间复杂度为O(n)。
七、拓展思考
如果题目要求“从右到左开始,再左到右交替”,只需修改反转条件:level % 2 === 0时反转,level % 2 === 1时不反转,核心逻辑不变。
另外,除了BFS,也可以用DFS(深度优先搜索)实现:记录当前层数,在递归遍历左右子节点时,根据层数决定将节点值存入当前层数组的开头还是结尾(奇数层存开头,偶数层存结尾),但BFS的思路更直观,也更符合层序遍历的本质,推荐优先掌握BFS解法。
总结
LeetCode 103题的核心是“层序遍历+层内反转”,没有复杂的算法设计,关键在于理解“锯齿形”的本质是层与层之间的方向交替,而不是改变节点的遍历顺序。掌握BFS的基本逻辑,再加上简单的反转操作,就能轻松解决这道题。
建议大家动手敲一遍代码,尝试修改层数计数方式、反转条件,加深对思路的理解。如果遇到复杂的二叉树案例,也可以画图模拟队列的入队、出队过程,帮助自己理清逻辑。