在数据结构的世界里,树是一种极具 “自然美感” 的非线性结构 —— 它不像数组、链表那样 “一条道走到黑”,而是像自然界的树一样,从根到枝、从枝到叶层层延展。作为算法面试的高频考点,树与二叉树不仅是基础数据结构,更是理解红黑树、B 树、堆等高级结构的钥匙。
本文将结合通俗比喻、可视化图解和逐行拆解的代码,从 “是什么” 到 “怎么用”,带初学者彻底吃透树与二叉树的核心知识点。无论你是刚入门编程的新手,还是想巩固基础的开发者,都能在这里找到清晰的学习路径~
一、树:非线性结构的 “自然抽象”
1. 从自然界到代码:树的核心映射
你一定见过自然界的树 —— 一根树干向上生长,分出无数树枝,树枝末端长着树叶。数据结构中的 “树”,正是对这种形态的抽象:
| 自然界的树 | 数据结构中的树 | 核心作用 |
|---|---|---|
| 树根 | 根节点(root) | 树的起点,唯一没有 “父节点” 的节点 |
| 树枝 | 边(edge) | 连接两个节点,代表节点间的关联关系 |
| 树枝分叉处 | 非叶子节点 | 能延伸出子节点的 “中间节点” |
| 树叶 | 叶子节点(leaf) | 没有子节点的 “终点节点” |
| 整棵树 | 树(Tree) | 由 n≥0 个节点和 n-1 条边组成的非线性结构(n=0 时为空树) |
关键特性:树中没有 “环”(类似树枝不会自己绕回树干),任意两个节点之间有且只有一条路径 —— 这是树与图的核心区别。
2. 树的核心概念:一次搞懂不混淆
初学者最容易在 “层次、高度、深度” 这些术语上迷路,这里用 “公司组织架构” 帮你直观理解:
plaintext
董事长(根节点)—— 第1层
/ \
部门经理A 部门经理B —— 第2层
/ \ \
员工1 员工2 员工3 —— 第3层
- 层次(Level) :从根节点开始计数,根节点是第 1 层(部分教材记为第 0 层,需注意场景),其子节点为第 2 层,依次递增。
- 深度(Depth) :从根节点到当前节点的 “路径长度”(节点数)。比如员工 1 的深度是 3(董事长→经理 A→员工 1)。
- 高度(Height) :从当前节点到最远叶子节点的 “路径长度”(节点数)。比如部门经理 A 的高度是 2(经理 A→员工 1),叶子节点的高度永远是 1。
- 度(Degree) :节点的 “子节点个数”。董事长的度是 2(管理 2 个经理),员工 1 的度是 0(无下属),树的度是所有节点度的最大值(此例为 2)。
- 叶子节点:度为 0 的节点(无子女),比如员工 1、员工 2、员工 3。
💡 记忆技巧:深度 “从根往下算”,高度 “从叶往上算”,层次是节点的 “所在楼层”。
二、二叉树:树的 “简化版王者”
二叉树是树中最常用的类型,它的 “简化规则” 让实现和算法变得异常高效 ——每个节点最多有两个子节点,且左、右子树顺序固定。
1. 二叉树的定义:递归是核心
二叉树的定义本身就充满了递归思想(这也是树的核心魅力):
- 二叉树可以是空树(没有任何节点);
- 若不为空,则由根节点、左子树、右子树组成,且左、右子树也必须是二叉树;
- 左子树和右子树有严格顺序,不能随意交换(比如 “左子树是 2,右子树是 3” 和 “左子树是 3,右子树是 2” 是两棵不同的二叉树)。
2. 关键误区:二叉树≠度为 2 的树
很多初学者会误以为 “二叉树就是每个节点度最多为 2 的树”,但这只说对了一半!核心区别在于:
- 度为 2 的树:不允许存在 “单个子节点”(比如一个节点只能有 0 或 2 个子节点,不能只有 1 个),且无左右顺序;
- 二叉树:允许存在 “单个子节点”(比如只有左子树或只有右子树),且左右顺序严格(左子树≠右子树)。
举个例子:一个节点只有右子树,这是合法的二叉树,但不是度为 2 的树。
3. 二叉树的节点结构:代码如何表示?
二叉树的节点需要存储 “数据” 和 “子节点引用”,在 JavaScript 中主要有 3 种表示方式,各有优劣:
方式 1:函数式构造函数(传统经典)
javascript
运行
// 定义二叉树节点构造函数
function TreeNode(val) {
this.val = val; // 数据域:存储节点值
this.left = this.right = null; // 引用域:左、右子节点初始化为null
}
// 构建一棵二叉树(数字版)
const root = new TreeNode(1); // 根节点
const node2 = new TreeNode(2); // 左子节点
const node3 = new TreeNode(3); // 右子节点
root.left = node2; // 根节点的左子树指向node2
root.right = node3; // 根节点的右子树指向node3
const node4 = new TreeNode(1); // node2的左子节点
const node5 = new TreeNode(1); // node2的右子节点
node2.left = node4;
node2.right = node5;
逐行解读:
function TreeNode(val):定义构造函数,参数val是节点的数据;this.val = val:给节点绑定数据域,存储具体值;this.left = this.right = null:初始化左、右子节点引用为null(新节点默认没有子节点);- 后续通过
root.left = node2这种赋值,建立节点间的关联,形成树结构。
方式 2:ES6 Class(语义清晰)
javascript
运行
class TreeNode {
constructor(val) {
this.val = val;
this.left = this.right = null;
}
}
// 构建字符版二叉树(后续遍历用)
const root = new TreeNode('A');
root.left = new TreeNode('B');
root.right = new TreeNode('C');
root.left.left = new TreeNode('D');
root.left.right = new TreeNode('E');
root.right.right = new TreeNode('F');
优势:符合现代 JavaScript 语法,语义更清晰,适合需要扩展节点方法(如isLeaf()判断是否为叶子节点)的场景。
方式 3:对象字面量(直观简洁)
javascript
运行
// 对象字面量直接表示树结构,一眼看穿层级关系
const tree = {
val: 1,
left: {
val: 2,
left: { val: 4, left: null, right: null },
right: { val: 5, left: null, right: null }
},
right: { val: 3, left: null, right: null }
};
优势:无需先定义构造函数,直接通过对象嵌套表示树结构,适合快速编写测试用例或临时树结构。
💡 实战建议:日常开发 / 面试中,对象字面量适合快速演示,Class 适合动态构建树(如插入、删除节点)。
三、二叉树的遍历:算法的 “核心操作”
遍历是二叉树最基础也最重要的操作 —— 指 “按一定顺序访问所有节点,且每个节点仅访问一次”。根据访问顺序的不同,分为深度优先遍历(DFS) 和广度优先遍历(BFS) 两大类,其中 DFS 又细分为前序、中序、后序遍历。
1. 先搞懂:为什么遍历需要 “递归”?
树的结构是递归定义的(一棵大树由无数棵小树组成),因此遍历树的最优方式就是递归 —— 把 “遍历整棵树” 拆分为 “遍历根节点 + 遍历左子树 + 遍历右子树”,直到遇到空节点。
递归遍历的三要素(必须牢记):
- 终止条件:遇到
null节点(没有子节点可遍历),直接返回; - 递推关系:遍历当前节点 → 遍历左子树 → 遍历右子树(或其他顺序);
- 返回值:根据需求返回(如收集节点值的数组、节点个数等)。
2. 深度优先遍历(DFS):“钻到底再回头”
深度优先遍历就像 “走迷宫”—— 沿着一条路径走到尽头(叶子节点),再回溯到上一个分叉口,走另一条路径。核心是用 “栈” 存储待访问节点(递归时隐式使用系统栈,迭代时手动用栈)。
(1)前序遍历:根 → 左 → 右
遍历顺序:先访问当前节点,再递归遍历左子树,最后递归遍历右子树。
javascript
运行
// 递归版前序遍历(返回节点值数组,方便后续复用)
function preorder(root) {
const result = []; // 存储遍历结果
// 内部递归函数:处理单个节点的遍历
function traverse(node) {
if (!node) return; // 终止条件:空节点直接返回
result.push(node.val); // 1. 访问当前节点(核心:根节点先入队)
traverse(node.left); // 2. 递归遍历左子树
traverse(node.right); // 3. 递归遍历右子树
}
traverse(root); // 从根节点开始遍历
return result;
}
// 测试:用字符版树(root = { val: 'A', left: ... })
console.log(preorder(root)); // 输出:["A", "B", "D", "E", "C", "F"]
逐行解读:
const result = []:创建数组存储遍历结果(避免直接打印,提高复用性);function traverse(node):内部递归函数,负责处理单个节点的遍历逻辑;if (!node) return:终止条件,空节点无需处理;result.push(node.val):访问当前节点,将值存入结果数组;traverse(node.left):递归遍历左子树(先钻到最左叶子节点);traverse(node.right):左子树遍历完后,递归遍历右子树。
可视化流程(以字符树为例):
- 访问根节点
A→ 结果:[A] - 遍历
A的左子树B→ 访问B→ 结果:[A, B] - 遍历
B的左子树D→ 访问D→ 结果:[A, B, D] D无左 / 右子树,回溯到B→ 遍历B的右子树E→ 访问E→ 结果:[A, B, D, E]E无左 / 右子树,回溯到A→ 遍历A的右子树C→ 访问C→ 结果:[A, B, D, E, C]- 遍历
C的右子树F→ 访问F→ 结果:[A, B, D, E, C, F]
(2)中序遍历:左 → 根 → 右
遍历顺序:先递归遍历左子树,再访问当前节点,最后递归遍历右子树(二叉搜索树的中序遍历是有序的,这是核心考点)。
javascript
运行
function inorder(root) {
const result = [];
function traverse(node) {
if (!node) return;
traverse(node.left); // 1. 先遍历左子树(钻到最左)
result.push(node.val); // 2. 访问当前节点(核心:左子树遍历完再访问根)
traverse(node.right); // 3. 遍历右子树
}
traverse(root);
return result;
}
console.log(inorder(root)); // 输出:["D", "B", "E", "A", "C", "F"]
关键提醒:中序遍历的核心是 “左子树优先”,比如遍历B节点时,必须先遍历完D(B的左子树),才会访问B。
(3)后序遍历:左 → 右 → 根
遍历顺序:先递归遍历左子树,再递归遍历右子树,最后访问当前节点。
javascript
运行
function postorder(root) {
const result = [];
function traverse(node) {
if (!node) return;
traverse(node.left); // 1. 遍历左子树
traverse(node.right); // 2. 遍历右子树
result.push(node.val); // 3. 最后访问当前节点(核心:左右都遍历完才访问根)
}
traverse(root);
return result;
}
console.log(postorder(root)); // 输出:["D", "E", "B", "F", "C", "A"]
记忆技巧:前、中、后序的区别仅在于 “访问根节点的时机”—— 前(根先)、中(根中)、后(根后),而左子树永远在右子树之前遍历。
3. 广度优先遍历(BFS):“逐层扫描不回头”
广度优先遍历就像 “从上到下扫楼”—— 先访问根节点(第 1 层),再访问第 2 层所有节点,接着第 3 层,直到所有层遍历完。核心是用 “队列” 存储待访问节点(队列先进先出,保证层级顺序)。
层序遍历:按层访问(面试高频)
javascript
运行
// 层序遍历(返回一维数组:所有节点按层从左到右)
function levelOrder(root) {
if (!root) return []; // 空树直接返回空数组
const result = []; // 存储遍历结果
const queue = [root]; // 队列:初始化存入根节点
// 队列不为空,说明还有节点待访问
while (queue.length) {
const node = queue.shift(); // 1. 队首节点出队(先进先出)
result.push(node.val); // 2. 访问当前节点
// 3. 左、右子节点入队(下一层节点)
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
return result;
}
// 测试:字符版树
console.log(levelOrder(root)); // 输出:["A", "B", "C", "D", "E", "F"]
逐行解读:
if (!root) return []:空树处理,避免后续报错;const queue = [root]:队列初始化,根节点是第一个待访问节点;while (queue.length):队列不为空时,循环处理节点;const node = queue.shift():队首节点出队(队列先进先出,保证按层访问);result.push(node.val):访问当前节点,存入结果;node.left && queue.push(node.left):左子节点入队(下一层的左节点先入队,保证从左到右);node.right && queue.push(node.right):右子节点入队。
进阶:按层分组输出(面试常考变形)
如果要求返回 “按层分组的数组”(如[[A], [B,C], [D,E,F]]),只需记录每层的节点数:
javascript
运行
function levelOrderGrouped(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;
}
console.log(levelOrderGrouped(root)); // 输出:[["A"], ["B", "C"], ["D", "E", "F"]]
4. 递归 vs 迭代:什么时候该用哪种?
| 遍历方式 | 递归版 | 迭代版 |
|---|---|---|
| 优点 | 代码简洁、逻辑清晰,符合树的递归本质 | 无栈溢出风险(递归深度过大时会触发栈溢出),性能更稳定 |
| 缺点 | 递归深度过大(如 10000 层树)会触发栈溢出 | 代码稍复杂,需要手动管理栈 / 队列 |
| 适用场景 | 树的深度较小,追求代码简洁 | 树的深度较大(如大型数据结构),要求稳定性 |
💡 面试建议:先写出递归版(快速得分),再补充迭代版(展示功底),面试官会更认可~
四、作者私藏:初学者避坑指南
1. 常见误区纠正
- ❌ 误区 1:二叉树是度为 2 的树 → ✅ 正确:二叉树允许单个子节点,且左右顺序严格;
- ❌ 误区 2:递归遍历只能打印节点 → ✅ 正确:应返回结果数组(如
result),提高代码复用性; - ❌ 误区 3:层序遍历只能用递归 → ✅ 正确:层序遍历本质是 BFS,必须用队列(递归不适合);
- ❌ 误区 4:
left和right可以随意交换 → ✅ 正确:二叉树的左右子树顺序固定,交换后是不同的树。
2. 记忆口诀(背会就能写代码)
- 前序遍历:根左右,先存根;
- 中序遍历:左根右,左完存根;
- 后序遍历:左右根,左右完存根;
- 层序遍历:队列存,先进先出。
3. 实战小练习(巩固所学)
给定如下二叉树,写出前序、中序、后序、层序遍历的结果:
javascript
运行
const testTree = {
val: 5,
left: { val: 3, left: { val: 2 }, right: { val: 4 } },
right: { val: 7, right: { val: 8 } }
};
答案:
- 前序:[5, 3, 2, 4, 7, 8]
- 中序:[2, 3, 4, 5, 7, 8]
- 后序:[2, 4, 3, 8, 7, 5]
- 层序:[5, 3, 7, 2, 4, 8]
五、总结:从基础到进阶的路径
树与二叉树的学习核心是 “理解递归本质 + 掌握遍历逻辑”:
- 入门:搞懂树的核心概念(层次、高度、深度)和二叉树的定义;
- 基础:掌握节点的三种表示方式,能手动构建二叉树;
- 核心:熟练写出前序、中序、后序(递归 + 迭代)和层序遍历;
- 进阶:学习二叉搜索树(BST)、平衡二叉树(AVL)、堆等高级结构(均基于二叉树扩展)。
如果本文对你有帮助,不妨动手敲一遍代码 —— 数据结构的学习,“实践” 永远是最好的老师。后续我会继续分享二叉搜索树、树的修改(插入 / 删除)等进阶内容,关注我,一起从算法小白成长为高手~
最后,祝大家在算法的世界里,既能 “钻得深”(DFS),也能 “看得远”(BFS)!🚀