【 算法-8 /Lesson75(2025-12-18)】树与二叉树:从自然抽象到程序实现的完整指南🌳

3 阅读6分钟

🌳在计算机科学中,树(Tree) 是一种极其重要的非线性数据结构。它源于对自然界树木的抽象,却在算法、数据库、编译器、人工智能等多个领域发挥着核心作用。本文将系统性地介绍树的基本概念、二叉树的定义与特性、节点结构、遍历方法,并结合代码示例深入剖析其递归本质与实现细节。


🌲 什么是树?

树是一种分层的、非线性的数据结构,由节点(Node) 和连接节点的边(Edge) 组成。它的结构模仿了自然界中的树——但方向是“倒置”的:

  • 根节点(Root) :位于最顶层,没有父节点。
  • 子节点(Child) :一个节点下方直接相连的节点。
  • 父节点(Parent) :子节点上方的直接连接点。
  • 叶子节点(Leaf) :没有子节点的末端节点。
  • 边(Edge) :连接两个节点的线段,代表父子关系。

💡 关键特性

  • 树中任意两个节点之间有且仅有一条路径。
  • 树是无环的连通图。
  • 一棵树可以为空(即没有节点)。

🔢 树的核心概念

层次(Level)

  • 根节点位于第1层
  • 其子节点为第2层,依此类推。
  • 某节点的层次 = 其父节点层次 + 1。

高度(Height)

  • 树的高度:从根节点到最远叶子节点的最长路径上的边数(或节点数,不同定义需注意)。
  • 在本文及多数教材中,高度通常从叶子开始向上计算,空树高度为0,单节点树高度为1
  • 节点的高度 = 以该节点为根的子树的高度。

度(Degree)

  • 一个节点的 = 它拥有的子节点数量
  • 树的度 = 所有节点中最大的度。
  • 叶子节点的度为0。

🌿 二叉树:树的特化形式

二叉树(Binary Tree) 是树的一种特殊类型,具有严格的结构约束:

定义(递归式)

  1. 二叉树可以是空树(null)。
  2. 若非空,则由一个根节点、一个左子树和一个右子树组成。
  3. 左子树和右子树本身也必须是二叉树

⚠️ 重要区别

  • 二叉树 ≠ “每个节点度为2的树”。
  • 二叉树强调左右顺序:即使某个节点只有一个子节点,也必须明确它是左孩子还是右孩子
  • 左右子树不可互换,顺序具有语义意义(如表达式树、搜索树)。

🧱 二叉树节点的结构

在编程中,二叉树节点通常用对象或结构体表示:

// 来自 1.js
function TreeNode(val) {
  this.val = val;       // 数据域
  this.left = null;     // 左子节点引用
  this.right = null;    // 右子节点引用
}

也可以用字面量对象简洁表达:

// 来自 1.js2.js
const tree = {
  val: 'A',
  left: {
    val: 'B',
    left: { val: 'D' },
    right: { val: 'E' }
  },
  right: {
    val: 'C',
    right: { val: 'F' }
  }
};

这种嵌套结构天然契合树的递归定义。


🔁 递归:理解树的灵魂

树的定义本身就是递归的——整体由相似的子部分构成。因此,递归是处理树问题最自然、最强大的工具

🔄 递归函数:在函数内部直接或间接调用自身的函数。

递归必须包含:

  1. 基础情况(Base Case) :终止条件,防止无限调用(如 if (!root) return)。
  2. 递归关系(Recursive Case) :将问题分解为更小的同类子问题(如处理左子树、右子树)。

所有树的遍历本质上都是递归过程的体现。


🚶‍♂️ 二叉树的遍历方式

遍历(Traversal)是指按某种顺序访问树中所有节点。根据访问根节点的时机不同,分为三种深度优先遍历(DFS)和一种广度优先遍历(BFS)。

1️⃣ 📌 先序遍历(Preorder Traversal)

访问顺序:根 → 左子树 → 右子树

  • 应用场景:复制树、打印目录结构(先打印当前目录名)、序列化树。
  • 递归实现(来自 2.js)
function preOrder(root) {
  if (!root) { return; }           // 基础情况
  console.log(root.val);           // 访问根
  preOrder(root.left);             // 递归遍历左子树
  preOrder(root.right);            // 递归遍历右子树
}
  • 对示例树 A(B(D,E), C(,F)),输出:A B D E C F

不变量:无论哪种遍历,左子树总在右子树之前被处理(这是二叉树“左右”顺序的体现)。


2️⃣ 📌 中序遍历(Inorder Traversal)

访问顺序:左子树 → 根 → 右子树

  • 应用场景:二叉搜索树(BST)的中序遍历结果是升序序列
  • 递归实现(来自 3.js)
function inOrder(root) {
  if (!root) { return; }
  inOrder(root.left);              // 先遍历左
  console.log(root.val);           // 再访问根
  inOrder(root.right);             // 最后遍历右
}
  • 对同一棵树,输出:D B E A C F

3️⃣ 📌 后序遍历(Postorder Traversal)

访问顺序:左子树 → 右子树 → 根

  • 应用场景:释放内存(先释放子节点再释放父节点)、计算目录大小(先算子目录再汇总)。
  • 递归实现(来自 4.js)
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 Traversal / BFS)

按树的层次从上到下、从左到右访问节点

  • 不是递归!而是使用队列(Queue)实现的迭代算法
  • 应用场景:查找最近公共祖先、按层打印、序列化/反序列化。
  • 实现(来自 5.js)
function levelOrder(root) {
  if (!root) { return []; }
  const queue = [root];   // 初始化队列,放入根
  const res = [];
  while (queue.length) {
    const node = queue.shift();      // 出队(FIFO)
    res.push(node.val);              // 访问
    if (node.left) queue.push(node.left);   // 左孩子入队
    if (node.right) queue.push(node.right); // 右孩子入队
  }
  return res;
}
  • 输出:A B C D E F

🧠 为什么用队列?

  • 队列的先进先出(FIFO) 特性保证了先访问上层节点,其子节点随后被处理,从而实现“逐层”遍历。

🧩 总结:树遍历的本质对比

遍历方式访问顺序实现方式典型用途
先序根 → 左 → 右递归复制树、前缀表达式
中序左 → 根 → 右递归BST排序、中缀表达式
后序左 → 右 → 根递归删除树、后缀表达式、求大小
层序按层从左到右队列迭代层级操作、最短路径(在树中)

🔑 共同点

  • 所有递归遍历都遵循“分治”思想:处理根 + 递归处理左右子树。
  • 基础情况均为 root === null
  • 左右子树的处理顺序固定(左先于右),体现二叉树的方向性。

🌟 结语

树,作为计算机科学中最优雅的数据结构之一,以其递归本质和层次结构,为我们建模现实世界提供了强大工具。而二叉树,作为其最常用的形式,通过先序、中序、后序、层序四种遍历方式,揭示了数据组织的不同视角。掌握这些概念与实现,不仅是理解高级算法(如AVL树、红黑树、堆、Trie)的基础,更是培养递归思维和问题分解能力的关键一步。

📚 建议实践

  • 手动画出文中示例树的结构。
  • 用纸笔模拟每种遍历的执行栈或队列变化。
  • 尝试将递归遍历改为迭代(使用显式栈)。
  • 思考:如何根据两种遍历结果重建二叉树?

树的世界,深邃而有序。愿你在枝繁叶茂的代码森林中,找到属于自己的那条清晰路径。🌲💻