从根到叶:用 JavaScript 轻松理解二叉树与遍历

57 阅读6分钟

从树根到树叶:用 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)是二叉树的基础情况。这使得我们可以用简洁的递归逻辑处理所有情形。


如何辨别一棵树是不是二叉树?

判断标准很简单,三问三答:

  1. 有没有且仅有一个根? → 是
  2. 每个节点的孩子数 ≤ 2? → 是
  3. 每个孩子的左右位置是否明确指定? → 是

只要满足这三点,就是二叉树。

反例:如果某个节点有三个孩子(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 吗?
  • 左右是否明确区分?

如果答案都是“是”,那么恭喜你——你已经能准确识别一棵二叉树了!🌳