前言
很多前端同学一听到「树」「二叉树」就头大,觉得这是后端或算法岗才需要掌握的东西。但实际上,DOM 树、组件树、路由树、抽象语法树(AST)......树结构在前端无处不在。
本文会用最通俗的语言,带你从零掌握树的核心概念和四种遍历方式,并附上可直接运行的 JavaScript 代码。
一、什么是树?从现实世界到数据结构
数据结构的「树」,本质是对现实世界树的一层抽象:
现实世界的树 数据结构的树
───────────── ─────────────
树根 ────────► 根节点(root)
树枝 ────────► 边(edge)
树枝两端 ────────► 节点(node)
树叶 ────────► 叶子节点(leaf)
关键区别 :数据结构中的树是倒着画的------根在最上面,叶子在最下面。这其实更符合我们的阅读习惯(从根开始,向下展开)。
二、二叉树:最基础也最重要
2.1 递归定义
二叉树的定义非常优雅,它是递归定义的:
- 它可以没有根节点,作为一棵空树存在
- 如果不是空树,它由根节点 + 左子树 + 右子树组成,且左右子树本身也是二叉树
这个定义的精妙之处在于:用二叉树来定义二叉树。这就是递归思想的核心。
2.2 为什么不能用「度为2的树」来定义?
很多教材会说二叉树是「每个节点最多有两个子节点的树」,但这不严谨。二叉树和度为2的树有本质区别:
- 二叉树严格区分左右:即使只有一个子节点,也要明确它是左孩子还是右孩子
- 左右子树不能交换:在二叉搜索树等结构中,左右子树的顺序决定了树的语义
💡 知识点:二叉树的左右位置是严格规定的,左就是左,右就是右,不能随意交换。这是它和普通有序树的根本区别。
三、树的核心概念(面试高频)
3.1 层次(Level)
markdown
① ← 第1层
/ \
② ③ ← 第2层
/ \ / \
④ ⑤ ⑥ ⑦ ← 第3层
- 根节点的层次为 1
- 其他节点的层次 = 父节点的层次 + 1
3.2 高度(Height)与深度(Depth)
这是一个非常容易混淆的知识点,我们用一张图讲清楚:
markdown
深度(从上往下数) 高度(从下往上数)
──────────────── ────────────────
① 深度=1 ① 高度=3
/ \ / \
② ③ 深度=2 ② ③ 高度=2
/ \ / \
④ ⑤ 深度=3 ④ ⑤ 高度=1
- 深度:从根节点到该节点经过的边数(或层数)
- 高度 :从该节点到最远叶子节点经过的边数
- 叶子节点的高度为 1
- 其他节点的高度 = max(左子树高度, 右子树高度) + 1
🎯 记忆口诀:深度从根往下看,高度从叶往上看。
3.3 度(Degree)
一个节点「开叉」出去多少个子树,就叫该节点的度。
markdown
度为2的节点:有左右两个子节点
度为1的节点:只有一个子节点(左或右)
度为0的节点:没有子节点 → 这就是叶子节点
3.4 叶子节点
度为 0 的节点就是叶子节点。它们处于树的末端,不再向下延伸。
四、JavaScript 中如何表示二叉树?
在 JS 中表示二叉树非常简单,只需要「三件套」:
javascript
function TreeNode(val) {
this.val = val; // 数据域:存什么数据
this.left = null; // 左侧子节点的引用
this.right = null; // 右侧子节点的引用
}
用对象字面量构造一棵树也非常直观:
javascript
const tree = {
val: 'A',
left: {
val: 'B',
left: { val: 'D', left: null, right: null },
right: { val: 'E', left: null, right: null }
},
right: {
val: 'C',
left: { val: 'F', left: null, right: null },
right: { val: 'G', left: null, right: null }
}
};
对应的树形结构:
css
A
/ \
B C
/ \ / \
D E F G
💡 知识点 :在 JS 中,树本质上就是一个嵌套的对象结构 。每个节点的
left和right指向它的子节点(也是同样的结构),这就形成了递归嵌套。
五、四种遍历方式(重中之重)
树的遍历分为两大类:深度优先(DFS) 和 广度优先(BFS)。
遍历口诀(一定要记住)
先左后右是铁律:无论哪种遍历,左子树永远在右子树之前访问。
5.1 前序遍历(Preorder):根 → 左 → 右
访问顺序:先访问根节点,再递归访问左子树,最后递归访问右子树。
javascript
function preorder(root) {
if (!root) return; // 递归退出条件
console.log('当前遍历节点:', root.val); // ① 先访问根
preorder(root.left); // ② 再访问左子树
preorder(root.right); // ③ 最后访问右子树
}
// 输出:A B D E C F G
应用场景:
- 复制一棵树(先创建根,再复制左右)
- 序列化树结构
- 展示目录结构(根目录在最前面)
5.2 中序遍历(Inorder):左 → 根 → 右
访问顺序:先递归访问左子树,再访问根节点,最后递归访问右子树。
javascript
function inorder(root) {
if (!root) return; // 递归退出条件
inorder(root.left); // ① 先访问左子树
console.log('当前遍历节点:', root.val); // ② 再访问根
inorder(root.right); // ③ 最后访问右子树
}
// 输出:D B E A F C G
应用场景:
- 二叉搜索树(BST)的中序遍历结果是有序的,这是 BST 最重要的性质
- 表达式树求值
🎯 核心理解:为什么 BST 中序遍历结果是升序的?因为 BST 的性质是「左 < 根 < 右」,而中序正好是「左 → 根 → 右」,遍历顺序天然与大小关系一致。
5.3 后序遍历(Postorder):左 → 右 → 根
访问顺序:先递归访问左子树,再递归访问右子树,最后访问根节点。
javascript
function postorder(root) {
if (!root) return; // 递归退出条件
postorder(root.left); // ① 先访问左子树
postorder(root.right); // ② 再访问右子树
console.log('当前遍历节点:', root.val); // ③ 最后访问根
}
// 输出:D E B F G C A
应用场景:
- 删除树(必须先删子节点,再删父节点,否则子节点就找不到了)
- 计算目录大小(先算子目录,再汇总到父目录)
- 自底向上的动态规划
5.4 层序遍历(Levelorder):逐层访问
层序遍历不用递归,而是用**队列(Queue)**来实现,属于广度优先搜索(BFS)。
javascript
function levelorder(root) {
const queue = []; // 队列 ------ 核心数据结构
const result = []; // 存放遍历结果
if (!root) return result;
queue.push(root); // 根节点入队
while (queue.length > 0) {
const node = queue.shift(); // 队头出队(先入先出)
result.push(node.val); // 访问当前节点
if (node.left) queue.push(node.left); // 左子节点入队
if (node.right) queue.push(node.right); // 右子节点入队
}
return result;
}
// 输出:A B C D E F G
应用场景:
- 求树的最小深度(逐层扫描,遇到的第一个叶子就是最小深度)
- UI 组件的按层渲染
- 社交网络中的「一度人脉、二度人脉」
💡 知识点:层序遍历的核心是**队列的 FIFO(先进先出)**特性。每一层的节点按顺序入队,按顺序出队,就自然实现了「逐层」的效果。
六、四序遍历对比总结
| 遍历方式 | 访问顺序 | 实现方式 | 典型应用 |
|---|---|---|---|
| 前序遍历 | 根→左→右 | 递归 | 复制树、序列化 |
| 中序遍历 | 左→根→右 | 递归 | BST 排序输出 |
| 后序遍历 | 左→右→根 | 递归 | 删除树、自底向上计算 |
| 层序遍历 | 逐层从左到右 | 队列(迭代) | 最短路径、按层渲染 |
一张图记住所有遍历:
css
A
/ \
B C
/ \ / \
D E F G
前序:A → B → D → E → C → F → G
中序:D → B → E → A → F → C → G
后序:D → E → B → F → G → C → A
层序:A → B → C → D → E → F → G
七、递归思想的精髓
树的遍历大量使用递归,那什么是递归?用一句话概括:
函数自己调用自己,将大问题拆解为相同模式的小问题。
递归三要素
以经典的爬楼梯问题为例(每次可以爬 1 阶或 2 阶,n 阶有多少种爬法):
javascript
// f(n) = f(n-1) + f(n-2)
function climbStairs(n) {
if (n === 1) return 1; // 退出条件
if (n === 2) return 2; // 退出条件
return climbStairs(n - 1) + climbStairs(n - 2); // 递归公式
}
① 自顶向下思考
把大问题拆成小问题。比如 climbStairs(10) 可以拆成 climbStairs(9) + climbStairs(8)。
scss
f(10)
/ \
f(9) f(8)
/ \ / \
f(8) f(7) f(7) f(6)
... ... ... ...
f(2)=2 f(1)=1
② 找到递归规律(递归公式)
规律是:走到第 n 阶 = 从第 n-1 阶走一步 + 从第 n-2 阶走两步
即 f(n) = f(n-1) + f(n-2)
③ 明确退出条件
没有退出条件就会无限递归 ,最终导致爆栈(Stack Overflow)------因为每次函数调用都会入栈,栈空间有限。
javascript
if (n === 1) return 1; // 只剩1阶,只有1种走法
if (n === 2) return 2; // 只剩2阶,有两种走法(1+1 或 2)
⚠️ 注意 :上面这个递归解法简洁易懂,但存在大量重复计算(比如
f(7)被计算了多次)。实际生产中可以加一个memo(备忘录)来缓存已计算的结果,这就是记忆化递归,也是动态规划的雏形。
八、递归调用与调用栈
每次函数调用都会在**调用栈(Call Stack)**中压入一个栈帧:
scss
climbStairs(5)
├── climbStairs(4)
│ ├── climbStairs(3)
│ │ ├── climbStairs(2) → 返回 2
│ │ └── climbStairs(1) → 返回 1
│ │ → 返回 2+1 = 3
│ └── climbStairs(2) → 返回 2
│ → 返回 3+2 = 5
└── ...
如果把问题规模设得太大(如 climbStairs(100000)),调用栈会非常深,直接爆栈。
🎯 延伸思考:树的遍历同理------如果树非常深(比如一条链状的二叉搜索树),递归遍历同样可能爆栈。这时可以考虑用迭代 + 手动维护栈来替代递归。
九、总结
本文从零开始,带你掌握了:
- 树的本质:现实世界树的抽象,倒着画的层级结构
- 二叉树的递归定义:要么空,要么根+左子树+右子树(左右严格区分)
- 核心概念:深度(从上往下)、高度(从下往上)、度(分叉数)、叶子节点
- JS 表示法 :对象字面量嵌套,
{val, left, right} - 四种遍历:前序(根左右)、中序(左根右)、后序(左右根)、层序(队列逐层)
- 递归思想:自顶向下 + 找规律 + 退出条件
树结构是算法体系中最重要的一环,后续的二叉搜索树、AVL 树、红黑树、堆、Trie 树等都建立在二叉树的基础上。掌握了本文的内容,你就有了扎实的根基。