数据结构:二叉树

49 阅读6分钟

在前端开发的日常工作中,树形结构无处不在。从浏览器渲染的 DOM 树,到 React/Vue 核心的 Virtual DOM,再到我们习以为常的 JSON 对象嵌套,本质上都是树形数据结构的应用。

然而,很多前端工程师在面对数据结构与算法时,往往止步于“能用数组解决一切”。二叉树(Binary Tree)作为非线性数据结构的基石,不仅是算法面试的必考题,更是理解高级概念(如堆、图、AST 语法树)的必经之路。

本文将结合严谨的计算机科学理论与现代 JavaScript 实践,带你彻底搞懂二叉树。

第一部分:什么是二叉树?不仅仅是“分两叉”

1. 严谨定义

在数据结构理论中,二叉树并不等同于“度为 2 的树”。二叉树是一种递归定义的数据结构:

  1. 它可以是空树。
  2. 如果不为空,它由一个根节点 (Root)  和两棵互不相交的、分别被称为左子树 (Left Subtree)  和右子树 (Right Subtree)  的二叉树组成。

关键点:二叉树是有序树。左子树和右子树的次序不能任意颠倒。即使某个节点只有一棵子树,也要区分它是左子树还是右子树。

2. 核心术语

为了与后端或算法岗同学高效沟通,我们需要掌握以下标准术语:

  • 节点 (Node) :数据的存储单元,包含数据域和指针域。

  • 度 (Degree)

    • 结点的度:一个结点拥有的子树个数(0, 1 或 2)。
    • 树的度:树中所有结点的度的最大值。
  • 叶子结点 (Leaf) :度为 0 的结点,即没有子树的结点。

  • 深度 (Depth) / 高度 (Height) :树中结点的最大层数(根节点通常记为第 1 层)。

3. 特殊形态:满二叉树与完全二叉树

这是面试中极易混淆的概念,也是理解**堆(Heap)**的基础。

  • 满二叉树 (Full Binary Tree)
    深度为 k 且有 2^k - 1 个结点的二叉树。
    特征:每一层都“铺满”了结点,不存在任何缺口。
  • 完全二叉树 (Complete Binary Tree)
    深度为 k 的有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 k 的满二叉树中编号从 1 至 n 的结点一一对应。
    特征:叶子结点只能出现在最下两层,且最下层的叶子结点集中在左侧

屏幕截图 2026-02-07 205611.png

4. 为什么前端需要了解这些性质?

在 C 语言等底层实现中,利用完全二叉树的性质,我们可以用数组(顺序存储)而非链表来存储树。
对于数组中任意位置 i 的元素(从 1 开始编号):

  • 左孩子的位置是 2 * i
  • 右孩子的位置是 2 * i + 1
  • 父节点的位置是 Math.floor(i / 2)

这种性质是二叉堆 (Binary Heap)  的实现基础,而堆又是高效实现优先级队列(如 React 的 Scheduler 调度任务)的关键结构。此外,还有一个重要公式:

对任何一棵二叉树,如果其叶子结点数为 n0,度为 2 的结点数为 n2,则必有 n0 = n2 + 1。


第二部分:JavaScript 中的二叉树表示

在 JavaScript 中,我们通常使用链式存储的思想来表示二叉树。随着语言的发展,有三种常见的表达方式:

1. 对象字面量 (Object Literal)

最直观,常用于编写测试用例或 LeetCode 的输入数据。

JavaScript

const tree = {
    val: 1,
    left: {
        val: 2,
        left: { val: 4, left: null, right: null },
        right: { val: 5, left: null, right: null }
    },
    right: {
        val: 3,
        left: null,
        right: null
    }
};

2. ES5 构造函数 (Function)

早期的经典写法。

JavaScript

function TreeNode(val) {
    this.val = val;
    // 从右向左赋值,初始化左右指针为 null
    this.left = this.right = null;
}

3. ES6 Class 语法 (推荐)

现代前端开发的标准写法,语义清晰,易于扩展。

JavaScript

class TreeNode {
    constructor(val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

// 构建示例
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);

第三部分:深度优先遍历 (DFS)

二叉树的定义是递归的,因此递归是操作二叉树最自然的方式。深度优先遍历分为三种,其核心区别在于根节点 (Root)  被访问的时机。

记忆口诀:所谓“前、中、后”,指的都是根节点什么时候打印。左右子树的顺序永远是“先左后右”。

屏幕截图 2026-02-07 210048.png

1. 前序遍历 (Pre-order)

顺序:根 -> 左 -> 右
应用:打印目录结构,复制树。

JavaScript

function preorder(root) {
    if (!root) return;
    
    // 1. 访问根节点
    console.log(root.val);
    // 2. 递归左子树
    preorder(root.left);
    // 3. 递归右子树
    preorder(root.right);
}

2. 中序遍历 (In-order)

顺序:左 -> 根 -> 右
应用:对于二叉搜索树 (BST) ,中序遍历可以得到有序的数列。

JavaScript

function inorder(root) {
    if (!root) return;
    
    inorder(root.left);
    console.log(root.val); // 根节点在中间访问
    inorder(root.right);
}

3. 后序遍历 (Post-order)

顺序:左 -> 右 -> 根
应用:计算文件系统大小(先统计子目录,再汇总到父目录),销毁树。

JavaScript

function postorder(root) {
    if (!root) return;
    
    postorder(root.left);
    postorder(root.right);
    console.log(root.val); // 根节点最后访问
}

第四部分:广度优先遍历 (BFS) —— 层序遍历

深度优先关注“钻得深”,而广度优先关注“铺得广”。层序遍历要求从上到下、从左到右依次访问。

实现痛点:递归本质上是利用函数调用栈 (Stack),适合 DFS。而 BFS 需要先进先出 (FIFO) 的特性,因此我们需要借助 队列 (Queue)

在 JavaScript 中,我们可以使用数组的 push (入队) 和 shift (出队) 来模拟队列。

image.png

JavaScript

function levelOrder(root) {
    if (!root) return [];
    
    const result = [];
    const queue = [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;
}

第五部分:实际应用场景

为什么前端面试总考二叉树?因为它不仅仅是刷题,而是解决复杂问题的思维模型。

  1. DOM 遍历与操作
    DOM 树本质上就是多叉树。当你使用 document.querySelectorAll 或者遍历 childNodes 时,你正在进行树的遍历。
  2. 对象深度克隆 (Deep Clone)
    JavaScript 的对象嵌套结构就是一棵树。手写 deepClone 函数时,我们通常使用递归(DFS)来遍历每个属性,如果属性是对象则继续递归,这与二叉树的先序遍历逻辑异曲同工。
  3. 抽象语法树 (AST)
    无论是 Babel 转译 ES6 代码,还是 ESLint 检查代码规范,第一步都是将代码解析为 AST。AST 是一棵极其复杂的树,对代码的修改本质上就是对这棵树的增删改查。

总结

二叉树是数据结构的敲门砖。通过本文,我们从底层的 C 语言定义出发,理解了满二叉树与完全二叉树的内存意义,并掌握了 JavaScript 中递归 (DFS) 与迭代 (BFS) 的标准写法。

学习建议
不要只看代码,请打开编辑器,手动实现一遍 TreeNode 类和四种遍历方式。当你能徒手写出层序遍历时,你就已经迈过了数据结构的第一道门槛。