从零理解树与二叉树:用 JS 带你手撕遍历和递归

前言

很多前端同学一听到「树」「二叉树」就头大,觉得这是后端或算法岗才需要掌握的东西。但实际上,DOM 树、组件树、路由树、抽象语法树(AST)......树结构在前端无处不在。

本文会用最通俗的语言,带你从零掌握树的核心概念和四种遍历方式,并附上可直接运行的 JavaScript 代码。


一、什么是树?从现实世界到数据结构

数据结构的「树」,本质是对现实世界树的一层抽象:

复制代码
现实世界的树              数据结构的树
─────────────            ─────────────
树根          ────────►  根节点(root)
树枝          ────────►  边(edge)
树枝两端       ────────►  节点(node)
树叶          ────────►  叶子节点(leaf)

关键区别 :数据结构中的树是倒着画的------根在最上面,叶子在最下面。这其实更符合我们的阅读习惯(从根开始,向下展开)。


二、二叉树:最基础也最重要

2.1 递归定义

二叉树的定义非常优雅,它是递归定义的:

  1. 它可以没有根节点,作为一棵空树存在
  2. 如果不是空树,它由根节点 + 左子树 + 右子树组成,且左右子树本身也是二叉树

这个定义的精妙之处在于:用二叉树来定义二叉树。这就是递归思想的核心。

2.2 为什么不能用「度为2的树」来定义?

很多教材会说二叉树是「每个节点最多有两个子节点的树」,但这不严谨。二叉树和度为2的树有本质区别:

  • 二叉树严格区分左右:即使只有一个子节点,也要明确它是左孩子还是右孩子
  • 左右子树不能交换:在二叉搜索树等结构中,左右子树的顺序决定了树的语义

💡 知识点:二叉树的左右位置是严格规定的,左就是左,右就是右,不能随意交换。这是它和普通有序树的根本区别。


三、树的核心概念(面试高频)

3.1 层次(Level)

markdown 复制代码
        ①  ← 第1层
       / \
      ②   ③  ← 第2层
     / \ / \
    ④ ⑤ ⑥ ⑦ ← 第3层
  • 根节点的层次为 1
  • 其他节点的层次 = 父节点的层次 + 1

3.2 高度(Height)与深度(Depth)

这是一个非常容易混淆的知识点,我们用一张图讲清楚:

markdown 复制代码
    深度(从上往下数)              高度(从下往上数)
    ────────────────              ────────────────
         ① 深度=1                      ① 高度=3
        / \                            / \
       ②   ③ 深度=2                   ②   ③ 高度=2
      / \                            / \
     ④   ⑤ 深度=3                   ④   ⑤ 高度=1
  • 深度:从根节点到该节点经过的边数(或层数)
  • 高度 :从该节点到最远叶子节点经过的边数
    • 叶子节点的高度为 1
    • 其他节点的高度 = max(左子树高度, 右子树高度) + 1

🎯 记忆口诀:深度从根往下看,高度从叶往上看。

3.3 度(Degree)

一个节点「开叉」出去多少个子树,就叫该节点的

markdown 复制代码
    度为2的节点:有左右两个子节点
    度为1的节点:只有一个子节点(左或右)
    度为0的节点:没有子节点 → 这就是叶子节点

3.4 叶子节点

度为 0 的节点就是叶子节点。它们处于树的末端,不再向下延伸。


四、JavaScript 中如何表示二叉树?

在 JS 中表示二叉树非常简单,只需要「三件套」:

javascript 复制代码
function TreeNode(val) {
    this.val = val;          // 数据域:存什么数据
    this.left = null;        // 左侧子节点的引用
    this.right = null;       // 右侧子节点的引用
}

用对象字面量构造一棵树也非常直观:

javascript 复制代码
const tree = {
    val: 'A',
    left: {
        val: 'B',
        left: { val: 'D', left: null, right: null },
        right: { val: 'E', left: null, right: null }
    },
    right: {
        val: 'C',
        left: { val: 'F', left: null, right: null },
        right: { val: 'G', left: null, right: null }
    }
};

对应的树形结构:

css 复制代码
          A
        /   \
       B     C
      / \   / \
     D   E F   G

💡 知识点 :在 JS 中,树本质上就是一个嵌套的对象结构 。每个节点的 leftright 指向它的子节点(也是同样的结构),这就形成了递归嵌套。


五、四种遍历方式(重中之重)

树的遍历分为两大类:深度优先(DFS)广度优先(BFS)

遍历口诀(一定要记住)

先左后右是铁律:无论哪种遍历,左子树永远在右子树之前访问。

5.1 前序遍历(Preorder):根 → 左 → 右

访问顺序:先访问根节点,再递归访问左子树,最后递归访问右子树。

javascript 复制代码
function preorder(root) {
    if (!root) return;                    // 递归退出条件
    console.log('当前遍历节点:', root.val); // ① 先访问根
    preorder(root.left);                  // ② 再访问左子树
    preorder(root.right);                 // ③ 最后访问右子树
}
// 输出:A B D E C F G

应用场景

  • 复制一棵树(先创建根,再复制左右)
  • 序列化树结构
  • 展示目录结构(根目录在最前面)

5.2 中序遍历(Inorder):左 → 根 → 右

访问顺序:先递归访问左子树,再访问根节点,最后递归访问右子树。

javascript 复制代码
function inorder(root) {
    if (!root) return;                    // 递归退出条件
    inorder(root.left);                   // ① 先访问左子树
    console.log('当前遍历节点:', root.val); // ② 再访问根
    inorder(root.right);                  // ③ 最后访问右子树
}
// 输出:D B E A F C G

应用场景

  • 二叉搜索树(BST)的中序遍历结果是有序的,这是 BST 最重要的性质
  • 表达式树求值

🎯 核心理解:为什么 BST 中序遍历结果是升序的?因为 BST 的性质是「左 < 根 < 右」,而中序正好是「左 → 根 → 右」,遍历顺序天然与大小关系一致。

5.3 后序遍历(Postorder):左 → 右 → 根

访问顺序:先递归访问左子树,再递归访问右子树,最后访问根节点。

javascript 复制代码
function postorder(root) {
    if (!root) return;                    // 递归退出条件
    postorder(root.left);                 // ① 先访问左子树
    postorder(root.right);                // ② 再访问右子树
    console.log('当前遍历节点:', root.val); // ③ 最后访问根
}
// 输出:D E B F G C A

应用场景

  • 删除树(必须先删子节点,再删父节点,否则子节点就找不到了)
  • 计算目录大小(先算子目录,再汇总到父目录)
  • 自底向上的动态规划

5.4 层序遍历(Levelorder):逐层访问

层序遍历不用递归,而是用**队列(Queue)**来实现,属于广度优先搜索(BFS)。

javascript 复制代码
function levelorder(root) {
    const queue = [];   // 队列 ------ 核心数据结构
    const result = [];  // 存放遍历结果
    if (!root) return result;

    queue.push(root);   // 根节点入队

    while (queue.length > 0) {
        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 G

应用场景

  • 求树的最小深度(逐层扫描,遇到的第一个叶子就是最小深度)
  • UI 组件的按层渲染
  • 社交网络中的「一度人脉、二度人脉」

💡 知识点:层序遍历的核心是**队列的 FIFO(先进先出)**特性。每一层的节点按顺序入队,按顺序出队,就自然实现了「逐层」的效果。


六、四序遍历对比总结

遍历方式 访问顺序 实现方式 典型应用
前序遍历 根→左→右 递归 复制树、序列化
中序遍历 左→根→右 递归 BST 排序输出
后序遍历 左→右→根 递归 删除树、自底向上计算
层序遍历 逐层从左到右 队列(迭代) 最短路径、按层渲染

一张图记住所有遍历

css 复制代码
            A
          /   \
         B     C
        / \   / \
       D   E F   G

前序:A → B → D → E → C → F → G
中序:D → B → E → A → F → C → G
后序:D → E → B → F → G → C → A
层序:A → B → C → D → E → F → G

七、递归思想的精髓

树的遍历大量使用递归,那什么是递归?用一句话概括:

函数自己调用自己,将大问题拆解为相同模式的小问题。

递归三要素

以经典的爬楼梯问题为例(每次可以爬 1 阶或 2 阶,n 阶有多少种爬法):

javascript 复制代码
// f(n) = f(n-1) + f(n-2)
function climbStairs(n) {
    if (n === 1) return 1;  // 退出条件
    if (n === 2) return 2;  // 退出条件
    return climbStairs(n - 1) + climbStairs(n - 2); // 递归公式
}

① 自顶向下思考

把大问题拆成小问题。比如 climbStairs(10) 可以拆成 climbStairs(9) + climbStairs(8)

scss 复制代码
                  f(10)
               /        \
           f(9)          f(8)
          /    \        /    \
       f(8)   f(7)   f(7)   f(6)
       ...     ...    ...    ...
      f(2)=2 f(1)=1

② 找到递归规律(递归公式)

规律是:走到第 n 阶 = 从第 n-1 阶走一步 + 从第 n-2 阶走两步

f(n) = f(n-1) + f(n-2)

③ 明确退出条件

没有退出条件就会无限递归 ,最终导致爆栈(Stack Overflow)------因为每次函数调用都会入栈,栈空间有限。

javascript 复制代码
if (n === 1) return 1;  // 只剩1阶,只有1种走法
if (n === 2) return 2;  // 只剩2阶,有两种走法(1+1 或 2)

⚠️ 注意 :上面这个递归解法简洁易懂,但存在大量重复计算(比如 f(7) 被计算了多次)。实际生产中可以加一个 memo(备忘录)来缓存已计算的结果,这就是记忆化递归,也是动态规划的雏形。


八、递归调用与调用栈

每次函数调用都会在**调用栈(Call Stack)**中压入一个栈帧:

scss 复制代码
climbStairs(5)
├── climbStairs(4)
│   ├── climbStairs(3)
│   │   ├── climbStairs(2) → 返回 2
│   │   └── climbStairs(1) → 返回 1
│   │   → 返回 2+1 = 3
│   └── climbStairs(2) → 返回 2
│   → 返回 3+2 = 5
└── ...

如果把问题规模设得太大(如 climbStairs(100000)),调用栈会非常深,直接爆栈。

🎯 延伸思考:树的遍历同理------如果树非常深(比如一条链状的二叉搜索树),递归遍历同样可能爆栈。这时可以考虑用迭代 + 手动维护栈来替代递归。


九、总结

本文从零开始,带你掌握了:

  1. 树的本质:现实世界树的抽象,倒着画的层级结构
  2. 二叉树的递归定义:要么空,要么根+左子树+右子树(左右严格区分)
  3. 核心概念:深度(从上往下)、高度(从下往上)、度(分叉数)、叶子节点
  4. JS 表示法 :对象字面量嵌套,{val, left, right}
  5. 四种遍历:前序(根左右)、中序(左根右)、后序(左右根)、层序(队列逐层)
  6. 递归思想:自顶向下 + 找规律 + 退出条件

树结构是算法体系中最重要的一环,后续的二叉搜索树、AVL 树、红黑树、堆、Trie 树等都建立在二叉树的基础上。掌握了本文的内容,你就有了扎实的根基。


相关推荐
LiuJun2Son1 小时前
Angular 快速入门:从零搭建你的第一个应用
前端·javascript·angular.js
YHL1 小时前
🚀从零理解树与二叉树 —— 概念、实现与遍历
前端·javascript·数据结构
十九画生1 小时前
学 JavaScript 数据类型,真正要搞懂的是:变量里存的到底是什么?
javascript
ZengLiangYi1 小时前
测试策略:单元测试 + 集成测试怎么写
javascript·typescript·node.js
JieE2122 小时前
JS 到底有多少种数据类型?从ECMA规范到内存本质,一文彻底搞懂
javascript·数据结构·面试
努力努力再努力wz2 小时前
【内存管理与高并发内存池系列】从 mmap 到 malloc:文件映射、匿名映射与 glibc 内存分配机制详解
linux·c语言·数据结构·数据库·c++·qt·链表
前端Hardy2 小时前
GitHub 爆火!Three.js + React + ECharts 打造最强数据大屏
前端·javascript
八解毒剂2 小时前
数据结构-平衡二叉树——对二叉搜索树的优化
数据结构·c++·算法
数据知道3 小时前
视觉伪装(下):WebGL 渲染器与厂商特征的底层伪造与屏蔽
javascript·数据采集·webgl·指纹浏览器