前言
在前端开发和算法面试中,二叉树是最常见、最基础却又极易出错的数据结构之一。很多同学初学时觉得“简单”,但一到手写遍历代码或变种题目就卡壳。本文将从零开始,由浅入深地带你彻底掌握二叉树的核心概念、定义、遍历方式,并结合递归思维和常见面试考点,帮助你真正理解而不是死记硬背。
一、什么是树?为什么需要树?
现实世界中很多数据天然就具有层级关系:
- 公司组织架构(CEO → 部门经理 → 员工)
- 文件系统(文件夹嵌套)
- DOM 树(HTML 元素嵌套)
- 家族族谱
线性结构(数组、链表)无法高效表达这种“一对多”的关系,于是就有了树形结构。
树的核心特点:
- 只有一个根节点(root)
- 每个节点可以有多个子节点(children)
- 没有环
- 从根到任意节点有且仅有一条路径
树的相关术语
- 节点(node):树的每一个元素
- 边(edge):连接节点的线
- 根节点(root):最顶层的节点
- 叶子节点(leaf):没有子节点的节点(度为 0)
- 节点的度(degree):该节点拥有的子树数量
- 树的深度/高度:从根节点到最远叶子节点的最长路径上的节点数(或边数,视定义而定)
- 层的概念:根节点为第 1 层,其子节点为第 2 层,以此类推
二、二叉树:最重要的一种树
二叉树(Binary Tree)是树家族中最常用的一种,每个节点最多只有两个子节点,分别称为左子节点(left)和右子节点(right)。
二叉树的严格定义(递归定义)
二叉树要用递归的方式来定义,这也是后面递归遍历的理论基础:
- 二叉树可以是空树(null)。
- 如果不是空树,则由三部分组成:根节点 + 左子树 + 右子树,且左子树、右子树本身也必须是合法的二叉树。
关键点:
- 左右子树有严格顺序,不能互换(不同于普通树)
- 每个节点的度最多为 2(不一定是正好 2,很多节点度为 1 或 0)
二叉树节点代码表示
JavaScript 中常见的两种写法:
// 类方式(更标准)
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
// 对象字面量方式(适合快速构建测试用例)
const root = {
val: "A",
left: {
val: "B",
left: { val: "D", left: null, right: null },
right: { val: "E", left: null, right: null }
},
right: {
val: "C",
right: { val: "F", left: null, right: null },
left: null
}
};
下面我们统一使用这棵树作为示例:
三、二叉树的遍历方式(重点!)
二叉树遍历是面试中最经典的题目,考察你对递归和迭代的掌握程度。
共有四种主流遍历方式:
1. 深度优先遍历(DFS)—— 前序、中序、后序(递归最自然)
深度优先的特点:尽可能深地探索子树,到底后再回溯。
三种顺序的区别仅在于根节点访问的时机不同:
(1)前序遍历(Preorder):根 → 左 → 右
输出顺序:A B D E C F
function preorder(root) {
if (!root) return;
console.log(root.val); // 先访问根
preorder(root.left); // 再左子树
preorder(root.right); // 最后右子树
}
(2)中序遍历(Inorder):左 → 根 → 右
输出顺序:D B E A C F (特别的:对二叉搜索树 BST,中序遍历结果是有序的!)
function inorder(root) {
if (!root) return;
inorder(root.left);
console.log(root.val); // 根在中间
inorder(root.right);
}
(3)后序遍历(Postorder):左 → 右 → 根
输出顺序:D E B F C A (应用场景:先处理子节点再处理父节点,如删除整个树、计算目录大小)
function postorder(root) {
if (!root) return;
postorder(root.left);
postorder(root.right);
console.log(root.val); // 根最后
}
递归思维拆解
以 preorder 为例,递归本质:
preorder(A)
├── print A
├── preorder(B)
│ ├── print B
│ ├── preorder(D) → print D → return
│ └── preorder(E) → print E → return
└── preorder(C)
├── preorder(null)
└── preorder(F) → print F → return
思考题:为什么递归写法如此简洁?因为二叉树的定义本身就是递归的,遍历问题天然就是“分治”问题:把大树的问题拆成左子树 + 右子树 + 根的处理。
2. 广度优先遍历(BFS)—— 层序遍历(Level Order)
广度优先的特点:一层一层地访问,常用队列实现。
输出:[[A], [B, C], [D, E, F]]
function levelOrder(root) {
if (!root) return [];
const result = [];
const queue = [root];
while (queue.length) {
const levelSize = queue.length;
const currentLevel = [];
for (let i = 0; i < levelSize; i++) {
const node = queue.shift();
currentLevel.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(currentLevel);
}
return result;
}
四、面试高频考点延伸
掌握以上基础后,面试中常考变形题:
-
非递归实现三种深度遍历(用栈模拟递归)
- 前序最简单,中序最难(需要记录访问状态)
-
给定前序 + 中序,能唯一恢复二叉树(经典构造题)
-
给定中序 + 后序,也能唯一恢复
-
给定前序 + 后序,大部分情况不能唯一恢复(特殊情况除外)
-
层序遍历的变种:之字形打印(奇偶层反向)、每一层返回最后一个节点等
-
二叉树的最大深度、最小深度、对称性判断、路径和等问题(递归分治思想)
-
Morris 遍历(O(1) 空间的线程二叉树遍历,较冷门)
五、总结与建议
二叉树遍历的核心在于:
- 理解递归定义 → 自然写出递归遍历
- 记住根节点访问的相对顺序 → 区分前/中/后序
- 队列 vs 栈 → 层序 vs 深度优先
学习建议:
- 手画一棵小树,亲自走一遍四种遍历,观察输出顺序。
- 尝试把递归改成迭代(栈),体会系统栈与显式栈的对应。
- LeetCode 上刷经典题:94(中序)、144(前序)、145(后序)、102(层序)、101(对称二叉树)、226(翻转二叉树)。
二叉树是递归思维的最佳入门载体,掌握它,你会发现后续的回溯、分治、动态规划都变得顺畅很多。