从现实到代码:深入理解树与二叉树
数据结构中的树,是对现实世界树形结构的一次精妙抽象
引言
在现实生活中,树随处可见------大树有根、有枝干、有树叶。计算机科学中的树结构正是从这一自然形态中汲取灵感,只不过它被倒置了:根在上,叶在下。
树结构在计算机领域应用广泛,从文件系统、DOM 树,到编译器语法分析、数据库索引,处处都有它的身影。今天,我们就来深入理解树与二叉树,并用 JavaScript 实现它的核心操作。
一、二叉树:递归定义的典范
1.1 什么是二叉树?
二叉树是树结构中最基础也最重要的一种。它的定义本身就是递归的:
- 它可以是一棵空树(没有根节点)
- 如果不是空树,那么它由根节点 、左子树 和右子树组成,且左右子树都是二叉树
⚠️ 注意:二叉树不能 简单地定义为"每个节点度不超过 2 的树"。二叉树的左右子树位置是严格约定的,不可交换。即使只有一个子节点,也要明确它是左孩子还是右孩子。
1.2 为什么用递归定义?
递归的核心思想是:将一个大问题分解为若干个相似的子问题。
scss
f(10)
/ \
f(9) f(8)
/ \ / \
f(8) f(7) f(7) f(6)
...
二叉树天然具有这种自相似性------一个节点下面又挂载着同样结构的子树,这正是递归的用武之地。
不过要记住:递归依赖函数调用栈,如果层数过深可能导致栈溢出(爆栈),后续我们会讨论优化方案。
二、树的核心概念速览
在深入二叉树之前,先来梳理几个树结构的通用术语:
| 概念 | 含义 |
|---|---|
| 层次 | 根节点所在层为第 1 层,子节点依次+1 |
| 高度 | 叶子节点高度为 1,向上逐层+1(目标节点高度 = 子节点高度 + 1) |
| 度 | 一个节点拥有子树的个数 |
| 叶子节点 | 度为 0 的节点(没有子节点) |
💡 记忆技巧:层次 从上往下数,高度从下往上数。
三、二叉树的分类与性质
3.1 两种特殊的二叉树
满二叉树(Full Binary Tree)
每一层的节点数都达到最大值。也就是说,除了叶子节点外,每个节点都有两个子节点。
markdown
1
/ \
2 3
/ \ / \
4 5 6 7
完全二叉树(Complete Binary Tree)
除了最后一层,其他层都是满的,且最后一层的节点都靠左排列。
markdown
1
/ \
2 3
/ \ /
4 5 6
3.2 重要性质
设二叉树的高度为 h(根节点高度为 1):
- 第 i 层最多有 2^(i-1) 个节点
- 高度为 h 的二叉树最多有 2^h - 1 个节点
- 叶子节点数 = 度为 2 的节点数 + 1
四、JavaScript 中如何表示二叉树?
在 JS 中,每个二叉树节点可以抽象为三个部分:
- 数据域(val)
- 左子节点引用(left)
- 右子节点引用(right)
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
五、二叉树的遍历
遍历是二叉树最核心的操作。按照访问根节点的时机 ,可以分为三类递归遍历,且始终遵循先左后右的原则。
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
中序遍历(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
后序遍历(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.2 层序遍历(迭代实现)
层序遍历按照从上到下、从左到右 的顺序访问每个节点,使用队列来实现。
javascript
function levelorder(root) {
if (!root) return [];
const queue = [];
const result = [];
queue.push(root);
while (queue.length) {
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']
六、递归思想与树:爬梯子问题
树的递归结构让我们自然联想到其他递归问题。来看一个经典例子------爬楼梯问题:
每次可以爬 1 或 2 个台阶,爬到第 n 级有多少种不同方法?
这个问题可以看作一棵递归树:
scss
f(5)
/ \
f(4) f(3)
/ \ / \
f(3) f(2) f(2) f(1)
...
递归公式与退出条件:
javascript
function climbStairs(n) {
if (n <= 2) return n; // 退出条件
return climbStairs(n - 1) + climbStairs(n - 2); // 递归公式
}
不过,这段代码在 n 较大时(如 n=100)会爆栈,因为存在大量重复计算。优化方案我们将在后续动态规划专题中讨论。
七、拓展:非递归遍历(迭代实现)
递归虽然代码简洁,但有栈溢出风险。以下给出前序遍历的迭代实现:
javascript
function preorderIterative(root) {
if (!root) return;
const stack = [root];
while (stack.length) {
const node = stack.pop();
console.log(node.val);
// 栈是 LIFO,先压右孩子,再压左孩子
if (node.right) stack.push(node.right);
if (node.left) stack.push(node.left);
}
}
中序和后序的迭代实现稍复杂,核心思路同样是手动维护栈来模拟函数调用栈。
八、小结
本文我们系统梳理了:
| 知识点 | 核心要点 |
|---|---|
| 二叉树定义 | 递归结构,左右有序 |
| 关键概念 | 层次、高度、度、叶子节点 |
| 存储方式 | JS 对象引用(链式存储) |
| 递归遍历 | 前/中/后序,先左后右 |
| 层序遍历 | 队列实现,逐层访问 |
| 递归思想 | 自顶向下,分解子问题 |
树结构是理解更复杂数据结构(如堆、AVL 树、红黑树、B 树)的基础,也是算法面试中的常客。掌握递归与树的结合,你将在编程之路上走得更远。

🚀 下期预告:二叉搜索树(BST)与动态规划优化------从爆栈到高效。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
