在数据结构的世界里,树是一种极具代表性的非线性结构,它模拟了现实世界中树的形态(只不过是 "倒过来" 的),广泛应用于数据库索引、编译器语法分析、文件系统管理等场景。其中,二叉树作为树结构中最基础也最常用的类型,更是程序员必须掌握的核心知识点。本文将从树的基本概念出发,详解二叉树的定义、表示方法,并结合 JavaScript 代码实例,讲解二叉树的遍历方式。
一、树的核心概念
1. 树的基本构成
树的结构可以拆解为三个核心要素:
- 根节点:树的最顶层节点,是整棵树的 "起点",没有父节点;
- 节点与边:树枝的两端为节点,节点存储数据,边表示节点间的父子关系;
- 叶子节点:没有子节点的节点(度为 0 的节点),位于树的最底层。
2. 二叉树的定义
二叉树是树的特殊形式,其定义具有递归特性:
- 可以是空树(没有任何节点);
- 如果非空,必须由根节点 、左子树 、右子树组成,且左、右子树本身也必须是二叉树;
- 二叉树的左右子树位置严格固定,不能随意交换,这是与普通树的关键区别。
3. 二叉树的关键术语
- 层次:根节点层次为 1,子节点层次为父节点层次 + 1;
- 高度:叶子节点高度为 1,向上每一层高度 + 1;
- 度:一个节点拥有的子树数量(二叉树节点的度最大为 2);
- 叶子节点:度为 0 的节点,即没有左、右子节点的节点。
二、JavaScript 中如何表示二叉树
在 JavaScript 中,二叉树的节点可以通过对象来表示,每个节点包含三部分:数据域(存储节点值)、左子节点引用、右子节点引用。
1. 定义二叉树节点
javascript
运行
kotlin
// 定义二叉树节点构造函数
function TreeNode(val) {
this.val = val; // 数据域:存储节点值
this.left = null; // 左子节点引用,初始为null
this.right = null; // 右子节点引用,初始为null
}
2. 构建一棵完整的二叉树
基于上述构造函数,我们可以手动构建一棵具体的二叉树:
javascript
运行
less
// 构建一棵示例二叉树
const tree = {
val: 'A', // 根节点
left: { // 左子树
val: 'B',
left: { // B的左子树
val: 'D',
left: null,
right: null
},
right: { // B的右子树
val: 'E',
left: null,
right: null
}
},
right: { // 右子树
val: 'C',
left: { // C的左子树
val: 'F',
left: null,
right: null
},
right: { // C的右子树
val: 'G',
left: null,
right: null
}
}
};
这棵树的结构如下:
plaintext
css
A
/ \
B C
/ \ / \
D E F G
三、二叉树的遍历
遍历是二叉树最核心的操作,目的是按特定顺序访问树中所有节点。根据访问顺序的不同,分为递归遍历 (前序、中序、后序)和迭代遍历(层序)两类。
1. 递归遍历:基于 "自顶向下" 的思想
递归遍历的核心是 "将大问题拆解为小问题":遍历整棵树 = 访问根节点 + 遍历左子树 + 遍历右子树,仅调整三者的顺序,就形成了不同的遍历方式。递归的关键是明确退出条件(节点为 null 时停止)。
(1)前序遍历:根节点 → 左子树 → 右子树
javascript
运行
scss
function preorder(root) {
// 退出条件:节点为null时,直接返回
if (!root) {
return;
}
// 第一步:访问根节点
console.log('前序遍历 - 当前节点:', root.val);
// 第二步:递归遍历左子树
preorder(root.left);
// 第三步:递归遍历右子树
preorder(root.right);
}
// 调用示例
preorder(tree); // 输出:A → B → D → E → C → F → G
(2)中序遍历:左子树 → 根节点 → 右子树
javascript
运行
scss
function inorder(root) {
// 退出条件:节点为null时,直接返回
if (!root) {
return;
}
// 第一步:递归遍历左子树
inorder(root.left);
// 第二步:访问根节点
console.log('中序遍历 - 当前节点:', root.val);
// 第三步:递归遍历右子树
inorder(root.right);
}
// 调用示例
inorder(tree); // 输出:D → B → E → A → F → C → G
(3)后序遍历:左子树 → 右子树 → 根节点
javascript
运行
scss
function postorder(root) {
// 退出条件:节点为null时,直接返回
if (!root) {
return;
}
// 第一步:递归遍历左子树
postorder(root.left);
// 第二步:递归遍历右子树
postorder(root.right);
// 第三步:访问根节点
console.log('后序遍历 - 当前节点:', root.val);
}
// 调用示例
postorder(tree); // 输出:D → E → B → F → G → C → A
2. 迭代遍历:层序遍历(按层次访问)
层序遍历不使用递归,而是借助队列(先进先出)实现,按 "从上到下、从左到右" 的顺序访问每一层节点。
javascript
运行
arduino
function levelorder(root) {
const queue = []; // 队列:存储待访问的节点
const res = []; // 结果数组:存储遍历结果
// 边界条件:空树直接返回空数组
if (!root) {
return res;
}
// 第一步:根节点入队
queue.push(root);
// 循环:队列不为空时,持续处理节点
while (queue.length > 0) {
// 出队:取出队列头部的节点
const node = queue.shift();
// 访问当前节点:将值存入结果数组
res.push(node.val);
// 左子节点存在则入队(先左后右)
if (node.left) {
queue.push(node.left);
}
// 右子节点存在则入队
if (node.right) {
queue.push(node.right);
}
}
// 返回遍历结果
return res;
}
// 调用示例
const levelResult = levelorder(tree);
console.log('层序遍历结果:', levelResult); // 输出:["A", "B", "C", "D", "E", "F", "G"]
四、递归的延伸:树状结构的解题思路
递归是处理树状结构的核心思想,其本质是 "找规律、定退出条件"。例如经典的 "爬楼梯问题",也可以用树状递归思路解决:
javascript
运行
scss
// 爬楼梯:n阶楼梯,每次走1或2阶,求总方法数
function climbStairs(n) {
// 退出条件:n≤2时,方法数等于n(1阶1种,2阶2种)
if (n <= 2) {
return n;
}
// 递归公式:f(n) = f(n-1) + f(n-2)
// 走n阶的方法数 = 走n-1阶的方法数 + 走n-2阶的方法数
return climbStairs(n - 1) + climbStairs(n - 2);
}
console.log(climbStairs(5)); // 输出:8(5阶楼梯有8种走法)
这个问题的递归过程可以看作一棵二叉树:每个节点代表 "剩余楼梯数",左子节点是 "走 1 阶",右子节点是 "走 2 阶",最终叶子节点的数量就是总方法数。
五、总结
树(尤其是二叉树)是数据结构的核心,其核心特性是递归定义 和非线性结构。在 JavaScript 中,我们可以通过对象轻松表示二叉树节点,而遍历操作则分为递归(前、中、后序)和迭代(层序)两种方式:
- 递归遍历的关键是 "先左后右" 的顺序和清晰的退出条件;
- 层序遍历的核心是借助队列实现 "按层访问"。
掌握树的基本概念和遍历方法,不仅能解决算法题,更能理解递归的本质 ------ 将复杂问题拆解为重复的小问题,这也是编程中最重要的思维方式之一。