二叉树与递归算法实战:从树结构到 LeetCode 爬楼梯,一文吃透前端数据结构与递归思维

二叉树与递归算法实战:从树结构到 LeetCode 爬楼梯,一文吃透前端数据结构与递归思维

🚀 树结构是前端面试的必考点,递归是处理树问题的最佳武器! 本文从二叉树的定义、关键概念、JS 表达方式出发,深入前序/中序/后序/层序四种遍历算法,并结合 LeetCode 经典"爬楼梯"问题,带你彻底掌握递归思维与树结构的核心知识。

📖 前言

在计算机科学中,树(Tree) 是一种非常重要的非线性数据结构。它模拟了现实世界中树的形态------只不过在计算机中,我们通常把它"倒过来"画:

css 复制代码
现实中的树:          计算机中的树:
    🌳                    A
   / | \                 / \
  枝 枝 枝              B   C
 /   |   \             / \ / \
叶   叶   叶           D  E F  G

树结构在前端开发中无处不在:

  • DOM 树(HTML 文档结构)
  • 组件树(React/Vue 组件嵌套)
  • 文件目录树
  • 路由配置树

理解树结构和递归算法,是成为优秀前端工程师的必修课


🧠 知识图谱

css 复制代码
二叉树与递归算法
├── 🌳 一、树的基本概念
│   ├── 根节点、边、节点、叶子节点
│   ├── 层次、高度、度
│   └── 二叉树的严格定义
│
├── 🏗️ 二、JS 中如何表达二叉树
│   ├── 构造函数方式
│   └── 对象字面量方式
│
├── 🔄 三、二叉树的四种遍历
│   ├── 前序遍历(Preorder)
│   ├── 中序遍历(Inorder)
│   ├── 后序遍历(Postorder)
│   └── 层序遍历(Level Order)
│
├── 🧮 四、递归思维:以爬楼梯为例
│   ├── 自顶向下分解问题
│   ├── 递归公式
│   └── 退出条件
│
└── ⚠️ 五、递归的陷阱与优化
    ├── 栈溢出风险
    └── 记忆化优化

🌳 一、树的基本概念

1.1 从现实世界到计算机世界

数据结构中的树,是对现实世界中树的简化抽象:

现实世界 计算机中的树
树根 根节点(Root)
树枝 (Edge)
树枝的两端 节点(Node)
树叶 叶子节点(Leaf)
css 复制代码
倒过来的树结构:

        A              ←── 根节点(Root)
       / \
      /   \
     B     C           ←── 子节点(Children)
    / \   / \
   D   E F   G         ←── 叶子节点(Leaf)

边:A-B, A-C, B-D, B-E, C-F, C-G

1.2 二叉树的严格定义

💡 二叉树不可以被简单定义为"每个节点的度为 2 的树" 。二叉树的左右子树位置是严格约定、不能交换的。

二叉树的递归定义:

  1. 它可以没有根节点,作为一棵空树存在
  2. 如果不是空树,那么必须由根节点左子树右子树组成,且左右子树都是二叉树
css 复制代码
二叉树的递归定义:

二叉树 = null(空树)
      或
二叉树 = {
    根节点,
    左子树: 二叉树,
    右子树: 二叉树
}

示例:
        A
       / \
      B   C
     / \ / \
    D  E F  G

A 的左子树:        A 的右子树:
    B                   C
   / \                 / \
  D   E               F   G

B 也是一棵二叉树(递归定义)

1.3 树的关键概念

概念 定义 示例
层次 根节点在第 1 层,子节点逐层加 1 A(1), B/C(2), D/E/F/G(3)
高度 叶子节点高度为 1,向上逐层加 1 D/E/F/G(1), B/C(2), A(3)
一个节点开叉出去的子树数量 A 的度为 2,D 的度为 0
叶子节点 度为 0 的节点(没有子节点) D、E、F、G
less 复制代码
层次与高度:

层次:          高度:
第1层  A        A: 3
      / \       
第2层 B   C      B: 2    C: 2
     / \ / \     
第3层 D E F G    D: 1    E: 1    F: 1    G: 1
                 (叶子节点高度为 1)

🏗️ 二、JS 中如何表达二叉树

2.1 构造函数方式

javascript 复制代码
function TreeNode(data) {
    this.data = data;           // 数据域
    this.left = this.right = null;  // 左右子节点引用
}

// 创建节点
const root = new TreeNode('A');
root.left = new TreeNode('B');
root.right = new TreeNode('C');

2.2 对象字面量方式(更直观)

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 复制代码
JS 对象与树结构的映射:

        {val:'A'}              ←── 根节点
       /         \
      /           \
{val:'B'}       {val:'C'}      ←── 子节点
   /   \         /   \
  /     \       /     \
{D}    {E}    {F}    {G}       ←── 叶子节点(left/right 都为 null)

每个节点对象包含三个部分:
┌─────────────────────────────┐
│  val: 节点值(数据域)        │
│  left: 左子节点引用           │
│  right: 右子节点引用          │
└─────────────────────────────┘

🔄 三、二叉树的四种遍历

遍历是指按某种顺序访问树中所有节点,且每个节点只访问一次。

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

💡 先访问根节点,再访问左子树,最后访问右子树。

javascript 复制代码
function preorder(root) {
    // 退出条件:遇到空节点
    if (!root) {
        return;
    }
    // ① 访问根节点
    console.log(`当前遍历节点是`, root.val);
    // ② 递归遍历左子树
    preorder(root.left);
    // ③ 递归遍历右子树
    preorder(root.right);
}

preorder(tree);
// 输出:A B D E C F G
css 复制代码
前序遍历过程:

        A
       / \
      B   C
     / \ / \
    D  E F  G

访问顺序:
① A(根)
  ② B(左子树的根)
    ③ D(左子树的左)
    ④ E(左子树的右)
  ⑤ C(右子树的根)
    ⑥ F(右子树的左)
    ⑦ G(右子树的右)

结果:A → B → D → E → C → F → G

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

💡 先访问左子树,再访问根节点,最后访问右子树。

javascript 复制代码
function inorder(root) {
    // 退出条件
    if (!root) {
        return;
    }
    // ① 递归遍历左子树
    inorder(root.left);
    // ② 访问根节点
    console.log(`当前遍历节点是`, root.val);
    // ③ 递归遍历右子树
    inorder(root.right);
}

inorder(tree);
// 输出:D B E A F C G
css 复制代码
中序遍历过程:

        A
       / \
      B   C
     / \ / \
    D  E F  G

访问顺序:
① D(最左)
② B(D 的根)
③ E(B 的右)
④ A(整棵树的根)
⑤ F(C 的左)
⑥ C(F 的根)
⑦ G(C 的右)

结果:D → B → E → A → F → C → G

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

💡 先访问左子树,再访问右子树,最后访问根节点。

javascript 复制代码
function postorder(root) {
    // 退出条件
    if (!root) {
        return;
    }
    // ① 递归遍历左子树
    postorder(root.left);
    // ② 递归遍历右子树
    postorder(root.right);
    // ③ 访问根节点
    console.log(`当前遍历节点是`, root.val);
}

postorder(tree);
// 输出:D E B F G C A
css 复制代码
后序遍历过程:

        A
       / \
      B   C
     / \ / \
    D  E F  G

访问顺序:
① D(最左叶子)
② E(D 的兄弟)
③ B(D、E 的根)
④ F(右子树的最左)
⑤ G(F 的兄弟)
⑥ C(F、G 的根)
⑦ A(整棵树的根,最后访问)

结果:D → E → B → F → G → C → A

3.4 层序遍历(Level Order):按层访问

💡 从根节点开始,按层访问节点,每层从左到右。 使用队列实现。

javascript 复制代码
function levelorder(root) {
    const queue = [];    // 队列
    const result = [];   // 结果数组

    if (!root) {
        return result;
    }

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

    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;
}

console.log(levelorder(tree));
// → ['A', 'B', 'C', 'D', 'E', 'F', 'G']
ini 复制代码
层序遍历过程(队列实现):

初始:queue = [A]

第1轮:
  出队 A,result = ['A']
  A.left = B 入队,A.right = C 入队
  queue = [B, C]

第2轮:
  出队 B,result = ['A', 'B']
  B.left = D 入队,B.right = E 入队
  queue = [C, D, E]

第3轮:
  出队 C,result = ['A', 'B', 'C']
  C.left = F 入队,C.right = G 入队
  queue = [D, E, F, G]

第4~7轮:
  依次出队 D、E、F、G(都没有子节点)
  result = ['A', 'B', 'C', 'D', 'E', 'F', 'G']

3.5 四种遍历对比

遍历方式 顺序 实现方式 应用场景
前序 根 → 左 → 右 递归 复制树、序列化
中序 左 → 根 → 右 递归 二叉搜索树排序
后序 左 → 右 → 根 递归 删除树、计算高度
层序 按层从左到右 队列(迭代) BFS、最短路径
ini 复制代码
遍历口诀:

前序:根左右  →  先打印根,再递归左右
中序:左根右  →  先递归左,再打印根,再递归右
后序:左右根  →  先递归左右,最后打印根

记忆技巧:
"前" = 根在最前
"中" = 根在中间
"后" = 根在最后

🧮 四、递归思维:以爬楼梯为例

4.1 问题描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。问有多少种不同的方法可以爬到楼顶?

4.2 自顶向下分解问题

scss 复制代码
爬楼梯问题的树状分解:

f(10) ------ 爬到第 10 阶的方法数
    │
    ├── 最后一步跨 1 阶 → f(9)
    │
    └── 最后一步跨 2 阶 → f(8)

f(10) = f(9) + f(8)

继续分解:

        f(10)
       /     \
    f(9)     f(8)
   /    \   /    \
 f(8)  f(7) f(7) f(6)
  │
  ...

f(1) = 1  (只有1种:跨1阶)
f(2) = 2  (2种:1+1 或 2)

4.3 递归三要素

javascript 复制代码
function climbStairs(n) {
    // ① 退出条件(边界条件)
    if (n == 1) {
        return 1;
    }
    if (n == 2) {
        return 2;
    }

    // ② 递归公式
    return climbStairs(n - 1) + climbStairs(n - 2);
}

console.log(climbStairs(10));  // → 89
要素 说明 示例
退出条件 递归的终止条件,防止无限递归 n == 1 返回 1,n == 2 返回 2
递归公式 将大问题分解为小问题的规律 f(n) = f(n-1) + f(n-2)
规模递减 每次递归问题规模都在减小 nn-1n-2

4.4 递归与树的联系

scss 复制代码
递归调用树:

        f(5)
       /    \
    f(4)    f(3)
   /    \   /   \
 f(3)  f(2) f(2) f(1)
  │
 f(2) f(1)

递归的本质就是树!
每个函数调用是一个节点
每次递归调用是子节点

💡 树状结构用递归:因为树本身就是递归定义的(每个子树也是树),所以递归是处理树问题的最自然方式。


⚠️ 五、递归的陷阱与优化

5.1 栈溢出风险

javascript 复制代码
// 递归深度过大时,会爆栈!
// climbStairs(10000) → RangeError: Maximum call stack size exceeded
scss 复制代码
递归调用栈:

 climbStairs(5)
  climbStairs(4)
   climbStairs(3)
    climbStairs(2)  ← 返回 2
    climbStairs(1)  ← 返回 1
   climbStairs(2)   ← 返回 2
  climbStairs(3)
   ...

每层递归都会占用栈内存
深度过大 → 栈内存耗尽 → 爆栈

5.2 记忆化优化

javascript 复制代码
// 使用记忆化(Memoization)避免重复计算
function climbStairsMemo(n, memo = {}) {
    // 退出条件
    if (n == 1) return 1;
    if (n == 2) return 2;

    // 如果已经计算过,直接返回缓存结果
    if (memo[n]) {
        return memo[n];
    }

    // 计算并缓存
    memo[n] = climbStairsMemo(n - 1, memo) + climbStairsMemo(n - 2, memo);
    return memo[n];
}

console.log(climbStairsMemo(100));  // → 可以快速计算!
优化方式 时间复杂度 空间复杂度 说明
纯递归 O(2^n) O(n) 大量重复计算
记忆化递归 O(n) O(n) 缓存中间结果
动态规划 O(n) O(1) 自底向上迭代

📊 核心知识速查表

树的关键概念

概念 定义
根节点 树的最顶层节点
叶子节点 度为 0 的节点(没有子节点)
节点拥有的子树数量
层次 根节点在第 1 层,逐层加 1
高度 叶子节点高度为 1,向上逐层加 1

遍历对比

遍历 顺序 根的位置 代码模式
前序 根 → 左 → 右 最先 console.log → 递归左 → 递归右
中序 左 → 根 → 右 中间 递归左 → console.log → 递归右
后序 左 → 右 → 根 最后 递归左 → 递归右 → console.log
层序 按层 --- 队列实现

递归三要素

要素 作用 示例
退出条件 终止递归 if (n <= 2) return n
递归公式 分解问题 f(n) = f(n-1) + f(n-2)
规模递减 向退出条件靠近 nn-1

💡 学习建议

  1. 画图理解:每次遇到树的问题,先在纸上画出树结构和遍历顺序
  2. 掌握递归三要素:退出条件、递归公式、规模递减缺一不可
  3. 前中后序对比记忆:记住"根"的位置------前(根在前)、中(根在中)、后(根在后)
  4. 层序用队列:BFS 问题优先考虑队列实现
  5. 注意栈溢出:递归深度过大时考虑记忆化或改迭代

📚 推荐阅读

  • MDN - 递归
  • LeetCode 94 --- 二叉树的中序遍历
  • LeetCode 102 --- 二叉树的层序遍历
  • LeetCode 70 --- 爬楼梯
  • 《数据结构与算法 JavaScript 描述》--- 树与二叉树

🏷️ 标签JavaScript 二叉树 递归 数据结构 算法 遍历 LeetCode 前端 面试


如果这篇文章帮你理解了二叉树和递归的本质,欢迎点赞 + 收藏 + 关注!有任何疑问欢迎在评论区交流~ 🎉

相关推荐
星栈2 小时前
Rust + Makepad 应用怎么打包发布:Windows、macOS、Linux 全平台交付
前端·rust
Irissgwe2 小时前
算法的时间复杂度和空间复杂度
数据结构·c++·算法·c·时间复杂度·空间复杂度
Aolith2 小时前
React 路由守卫:我用一个组件替代了 Vue 的 beforeEach
前端·react.js
Daybreak2 小时前
从 PDD、DDD、SDD 到 TDD:我是如何用一套 Agent 工程方法论推进 My-Notion 的
前端
HjhIron2 小时前
从零实现一个待办事项应用:前端必学的Ajax与Node.js实战
前端·后端
yingyima2 小时前
JavaScript 正则表达式:从零开始的实战对比
前端
Sammyyyyy3 小时前
月之暗面 Kimi Code 0.4.0 发布,终端 AI 编码助手全面采用 TypeScript,实现毫秒级启动
前端·javascript·人工智能·ai·typescript·servbay
范什么特西3 小时前
配置文件xml和properties
xml·前端
qq_297574673 小时前
设计模式系列文章(基础篇第22篇):访问者模式——分离数据结构与操作,实现灵活扩展
数据结构·设计模式·访问者模式