写在前面
二叉树是数据结构中最经典、最常考的非线性结构之一。无论是 LeetCode 刷题、技术面试,还是实际工程中的表达式解析、文件系统建模,几乎都能看到它的身影。
然而,很多初学者在面对“前序、中序、后序、层序”这些术语时容易混淆,写递归时更是频频踩坑:空指针报错、逻辑顺序颠倒、边界条件遗漏……看似简单的题目,一上手就出错。
本系列将从最基础的遍历出发,逐步深入对称性、深度、路径等高频问题,带你真正理解二叉树的递归本质与遍历逻辑。
这是第一篇——我们先打好地基。
二叉树的三种深度优先遍历(递归实现)
二叉树的前序、中序和后序遍历都属于深度优先搜索(DFS),区别仅在于访问当前节点的时机不同。以下是它们的标准递归写法。
前序遍历: 根 → 左 → 右
var preorderTraversal = function(root) {
let result = [];
const inorder = function(root) {
if(root === null) return;
result.push(root.val);
inorder(root.left);
inorder(root.right);
}
inorder(root);
return result;
};
中序遍历(左 → 根 → 右)
var preorderTraversal = function(root) {
let result = [];
const inorder = function(root) {
if(root === null) return;
inorder(root.left);
result.push(root.val);
inorder(root.right);
}
inorder(root);
return result;
};
后序遍历 左 → 右 → 根
var preorderTraversal = function(root) {
let result = [];
const inorder = function(root) {
if(root === null) return;
inorder(root.left);
inorder(root.right);
result.push(root.val);
}
inorder(root);
return result;
};
这三种遍历结构高度相似,唯一区别就是 push 语句的位置。掌握这个规律,就能轻松写出任意一种遍历方式。
提交结果(前序遍历)
层序遍历:
除了前序、中序和后序这些深度优先的遍历方法外,二叉树还有一种重要的遍历策略:层序遍历。
所谓层序遍历,就是按照树的层次结构,从根节点开始,自上而下、每一层从左到右依次访问所有节点。这种方式不沿着某一条路径深入到底,而是“横向”推进,逐层展开。
要高效实现这种遍历,通常需要借助队列这一数据结构。
因为队列遵循“先进先出”的原则,能自然地维持节点访问的顺序:先入队的节点(位于上层或靠左)会先被处理,从而确保遍历按层有序进行。这与使用栈实现的深度优先遍历形成鲜明对比。
值得一提的是,层序遍历本质上就是图论中的广度优先搜索(BFS),只是在二叉树这一特殊结构上的具体体现。
具体代码如下:
var levelOrder = function(root) {
let res =[], queue=[];
queue.push(root);
if(root === null) {
return res;
}
while(queue.length !== 0) {
let curLevel = [];
let length = queue.length;
for(let i = 0; i < length;i++) {
let node = queue.shift();
curLevel.push(node.val)
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
res.push(curLevel);
}
return res;
};
提交结果:
二叉树的性质:
对称问题
原题在这:101. 对称二叉树 - 力扣(LeetCode) 这道题看起来很简单,当你看到最终代码时,甚至会觉得“不过如此”。
但别小看它——判断逻辑和递归结构非常容易出错,尤其是在面试高压环境下,稍不注意就会踩坑。
先来看一段正确实现:
var isSymmetric = function(root) {
if(!root) return true;
const compareNode = function(left,right) {
if(!left && !right) return true;
if(!left || !right) return false;
if(left.val !== right.val) return false;
let outside = compareNode(left.left,right.right);
let inside = compareNode(left.right,right.left);
return outside && inside;
}
return compareNode(root.left,root.right)
};
这段代码虽然短,但每一行都值得细品。下面说两个最容易出错的关键点。
if(!root) return true;
注意!这个边界判断必须放在 isSymmetric 函数里,不能放进 compareNode。
if(!left && !right) return true;
if(!left || !right) return false;
if(left.val !== right.val) return false;
你可能会担心:
“第二行
if (!left || !right)会不会把left和right都为null的情况误判成false?”
不会!
因为在执行到第二行之前,第一行已经把“两者都为空”的情况拦截并返回 true 了。
所以第二行实际处理的是:“不是都空,但至少有一个是空”——也就是一个空、一个非空,显然不对称。
如果顺序写反了,比如先写 if (!left || !right) return false,那么当两个节点都为 null 时,会错误地返回 false,导致整个判断失败。
此外,必须在确认 left 和 right 都非空之后,才能安全访问 .val。否则会触发 Cannot read property 'val' of null 错误。
let outside = compareNode(left.left,right.right);
let inside = compareNode(left.right,right.left);
这是判断二叉树是否对称的核心逻辑,也是这道题的“题眼”
镜像对称的定义是:
左子树的左边 应该等于 右子树的右边,
左子树的右边 应该等于 右子树的左边。 对称二叉树的递归本质是:
“左子树的左” 与 “右子树的右” 对称,且 “左子树的右” 与 “右子树的左” 对称。
求最大、最小深度问题
选一道经典题目:104. 二叉树的最大深度 - 力扣(LeetCode)
var maxDepth = function(root) {
if (root === null) return 0; // 如果节点为空,则深度为0
// 计算左子树和右子树的最大深度
const leftDepth = maxDepth(root.left);
const rightDepth = maxDepth(root.right);
// 当前节点的最大深度等于左右子树的最大深度加1
return Math.max(leftDepth, rightDepth) + 1;
};
基础情况:如果当前节点是 null,则返回深度 0。
递归步骤:
对于每个节点,分别计算它的左子树和右子树的最大深度。
然后取这两者的较大值,加上当前节点本身(即加 1),得到以该节点为根的子树的最大深度。
最终结果:从根节点开始递归调用此函数,最终会返回整个树的最大深度。
这种方法简洁且高效,利用了递归自然地解决了深度优先搜索的问题,适用于大多数二叉树的深度计算场景
到这里,我们已经掌握了二叉树的四种基本遍历方式,也解决了对称性和最大深度这两个经典问题。你会发现,几乎所有二叉树的递归解法,都建立在“分治 + 子问题返回值”的思想之上。
但二叉树的世界远不止于此。
在接下来的文章中,我们将继续探索
基础打牢,进阶不慌。
下一篇,我们不见不散。