从树根到树叶:用 JavaScript 轻松理解二叉树与遍历
在编程世界里,树是一种非常重要的非线性数据结构。它不像数组那样一维排列,也不像链表那样首尾相接,而是像自然界中的大树一样——有根、有枝、有叶,层层展开。但和自然界的树“头朝上”不同,计算机里的树是“头朝下”的:根在最上面,叶子在最下面。
今天,我们就用 JavaScript 来揭开“树”的神秘面纱,尤其聚焦于一种最常用、最经典的树结构:二叉树。我们将从树的基本定义出发,逐步深入到二叉树的特性与遍历方式,让你不仅会写代码,更真正理解“树”为何如此设计。
“掌握二叉树,不仅能帮你理解文件系统、DOM 树等真实场景,还是面试算法题的基石。”
一棵树,只能有一个根
首先,请记住一个核心原则:一棵树只能有一个根节点(root) 。这就像一棵真实的树,无论枝繁叶茂到什么程度,它都只有一个主干从地里长出来。如果出现两个“根”,那就不是一棵树,而是两棵树(或者说是一个“森林”)。
在程序中,我们通过一个变量(比如 root)来指向这唯一的根节点。所有其他节点,都是从这个根“派生”出来的后代。
✅ 正确:
const root = new TreeNode(1);—— 一棵树
❌ 错误:同时存在两个互不相连的根节点 —— 那是两棵树
节点、边与度:树的基本构件
树由节点(node)和连接它们的边(edge)组成。每个节点可以有零个或多个子节点。
这里引入一个关键概念:度(Degree) 。
- 一个节点的度,是指它有多少个直接子节点。
- 叶子节点(leaf)就是度为 0 的节点——它没有孩子,是树的“末端”。
- 根节点的度可能是 1、2、3……甚至更多,取决于树的类型。
例如:
A ← 度为 2
/ \
B C ← B 度为 0(叶子),C 度为 2
/ \
D E ← D、E 都是叶子(度为 0)
整棵树的高度是从根到最远叶子的最长路径上的边数(有时也按节点数算,需注意上下文)。而层次从根开始算第 1 层,往下依次 +1。
什么是二叉树?它有什么特别?
二叉树(Binary Tree)是树的一种特殊形式,但它有严格的规则:
一棵二叉树,要么是空树,要么由一个根节点、一棵左子树和一棵右子树组成,且左右子树本身也必须是二叉树。
关键点来了:
1. 每个节点最多只有两个子节点
这不是说“必须有两个”,而是“不能超过两个”。所以一个节点可以有:
- 0 个孩子(叶子)
- 1 个孩子(只有左 or 只有右)
- 2 个孩子(既有左也有右)
2. 左右是有顺序的!
这是二叉树最特别的地方。左子树 ≠ 右子树,交换左右就变成另一棵不同的二叉树。
举个例子:
A vs. A
/ \
B B
这两棵树虽然结构相似,但在二叉树的世界里,它们是完全不同的!因为左和右的位置具有语义意义。
⚠️ 常见误区:二叉树 ≠ 度为 2 的树
度为 2 的树只要求每个节点最多有两个孩子,但不区分左右;而二叉树强制区分左右,即使某个节点只有一个孩子,你也必须明确它是“左孩子”还是“右孩子”。
3. 空树也是合法的二叉树
在递归定义中,空树(null)是二叉树的基础情况。这使得我们可以用简洁的递归逻辑处理所有情形。
如何辨别一棵树是不是二叉树?
判断标准很简单,三问三答:
- 有没有且仅有一个根? → 是
- 每个节点的孩子数 ≤ 2? → 是
- 每个孩子的左右位置是否明确指定? → 是
只要满足这三点,就是二叉树。
反例:如果某个节点有三个孩子(A → B, C, D),那它就不是二叉树,而是“三叉树”或一般的“多叉树”。
用 JavaScript 表达二叉树
在 JavaScript 中,我们可以用对象字面量或构造函数来表示树的节点。比如:
const root = {
val: 'A',
left: {
val: 'B',
left: { val: 'D' },
right: { val: 'E' }
},
right: {
val: 'C',
right: { val: 'F' }
}
};
这棵树看起来像这样:
A
/ \
B C
/ \ \
D E F
或者,我们也可以用构造函数的方式创建节点:
function TreeNode(val) {
this.val = val;
this.left = this.right = null;
}
const root = new TreeNode(1);
const node1 = new TreeNode(2);
const node2 = new TreeNode(3);
root.left = node1;
root.right = node2;
const node4 = new TreeNode(4);
const node5 = new TreeNode(5);
node2.left = node4;
node2.right = node5;
无论哪种方式,核心思想都一样:每个节点知道自己的值,以及左右孩子的“地址” 。
遍历二叉树:四种走法
既然树是非线性的,如何系统地访问每一个节点?这就需要遍历。二叉树有四种经典遍历方式。
1. 前序遍历(Preorder):根 → 左 → 右
先处理自己,再处理孩子。适合用于复制树或打印目录结构。
function preorder(root) {
if (!root) return;
console.log(root.val); // 先访问根
preorder(root.left); // 再左
preorder(root.right); // 最后右
}
// 输出:A B D E C F
2. 中序遍历(Inorder):左 → 根 → 右
先处理左子树,再自己,最后右子树。在二叉搜索树(BST)中,中序遍历结果是升序排列!
function inorder(root) {
if (!root) return;
inorder(root.left);
console.log(root.val); // 中间访问根
inorder(root.right);
}
// 输出:D B E A C F
3. 后序遍历(Postorder):左 → 右 → 根
先处理所有孩子,最后处理自己。常用于释放内存或计算目录总大小(先算子目录,再加总)。
function postorder(root) {
if (!root) return;
postorder(root.left);
postorder(root.right);
console.log(root.val); // 最后访问根
}
// 输出:D E B F C A
4. 层序遍历(Level Order):从上到下,从左到右
按层级访问,使用队列实现(先进先出):
function levelorder(root) {
if (!root) return [];
const queue = [root];
const result = [];//存储遍历过程中访问到的节点值
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']
总结:二叉树的核心要点
| 概念 | 说明 |
|---|---|
| 唯一根 | 一棵树只能有一个根节点 |
| 度 | 节点的子节点数量;叶子节点度为 0 |
| 二叉树定义 | 每个节点最多两个孩子,且严格区分左/右 |
| 左右不可交换 | 左子树和右子树位置有语义,交换即不同树 |
| 空树合法 | null 是二叉树的合法形态,用于递归终止 |
二叉树之所以重要,不仅因为结构清晰,更因为它支撑了大量高效算法(如二叉搜索树、堆、AVL 树、红黑树等)。掌握它的基本概念和遍历方法,是你迈向高级数据结构的第一步。
下次当你看到一棵树,不妨问问自己:
- 它有且仅有一个根吗?
- 每个节点的孩子数 ≤ 2 吗?
- 左右是否明确区分?
如果答案都是“是”,那么恭喜你——你已经能准确识别一棵二叉树了!🌳