直接一文搞懂二叉树与遍历实现

0 阅读6分钟
屏幕截图 2026-06-14 231515.png

在前端开发中,DOM树、虚拟DOM Diff、路由匹配等场景都离不开树形结构。本文基于数据结构中最基础的「二叉树」,从概念到JS实现逐层拆解,帮你彻底吃透核心知识点。


一、树形结构:现实世界的抽象简化

我们熟悉的家谱、公司组织架构、文件夹目录,本质上都是「树」的具象形态。数据结构的树是对现实的简化抽象:

  • 树根 → 根节点(唯一入口,没有父节点)
  • 树枝 → 边(连接两个节点的关系)
  • 树枝两端的分叉 → 子节点
  • 末端没有分叉的细枝 → 叶子节点

和现实中的树相反,数据结构中的树是「根在上、枝叶在下」的倒置结构,所有节点都通过唯一的路径连接到根节点。


二、二叉树:特殊的树形结构

普通树的子节点没有顺序限制,而二叉树是每个节点最多有2个子树,且左右子树严格区分顺序的特殊树结构,这也是它和普通「度为2的树」最核心的区别:哪怕只有一个子节点,也必须明确是左孩子还是右孩子,交换左右子树会得到完全不同的二叉树。

二叉树的递归定义

二叉树天然适配递归思想,我们可以用递归的方式严格定义它,这也对应了递归实现的三个核心要素:

  1. 自顶向下拆分大问题:把一棵复杂的二叉树拆解为「根节点 + 左子树 + 右子树」三个同构的小问题
  2. 问题处理逻辑一致:无论子树多大,处理逻辑都是「访问根 → 遍历左子树 → 遍历右子树」
  3. 明确的退出条件:当遇到空节点时,停止递归

⚠️ 注意:二叉树允许为空树(没有根节点),这是它的特殊形态。


三、二叉树核心概念

掌握这几个基础概念,后续理解和实现遍历会更轻松:

概念定义
层次根节点所在层为第1层,子节点每深入一层,层次+1
高度叶子节点高度为1,每向上回溯一层高度+1,整棵树的高度等于最大层次数
节点的度一个节点拥有的子节点数量,二叉树的节点度最大为2
叶子节点度为0的节点,也就是最后一层的末端节点

四、JS实现二叉树节点

二叉树的每个节点只需要包含三部分信息:存储的数据、左子节点的引用、右子节点的引用,用ES6 Class可以很直观地定义:

class TreeNode {
  constructor(val, left = null, right = null) {
    this.val = val;       // 数据域:存储节点值
    this.left = left;     // 左子节点引用
    this.right = right;   // 右子节点引用
  }
}

我们可以先构造一个示例二叉树,后续所有遍历方法都会基于这个结构验证结果:

// 构造如下结构的二叉树:
//       1
//     /   \
//    2     3
//   / \
//  4   5
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);

五、二叉树的遍历(核心重点)

遍历是指按某种规则依次访问树中所有节点,且每个节点仅访问一次。根据访问根节点的时机不同,分为三类递归遍历,另外还有按层访问的迭代遍历。

5.1 递归遍历(最直观)

递归遍历的核心差异只有「访问根节点的顺序」,只要记住这个规则,代码几乎不用改:

① 前序遍历(根 → 左 → 右)

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

function preorderTraversal(root) {
  const res = [];
  function dfs(node) {
    if (!node) return; // 递归退出条件:遇到空节点停止
    res.push(node.val); // 第一步:访问根节点
    dfs(node.left);     // 第二步:遍历左子树
    dfs(node.right);    // 第三步:遍历右子树
  }
  dfs(root);
  return res;
}
// 输出:[1, 2, 4, 5, 3]

② 中序遍历(左 → 根 → 右)

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

function inorderTraversal(root) {
  const res = [];
  function dfs(node) {
    if (!node) return;
    dfs(node.left);      // 第一步:遍历左子树
    res.push(node.val); // 第二步:访问根节点
    dfs(node.right);     // 第三步:遍历右子树
  }
  dfs(root);
  return res;
}
// 输出:[4, 2, 5, 1, 3]

③ 后序遍历(左 → 右 → 根)

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

function postorderTraversal(root) {
  const res = [];
  function dfs(node) {
    if (!node) return;
    dfs(node.left);      // 第一步:遍历左子树
    dfs(node.right);     // 第二步:遍历右子树
    res.push(node.val); // 第三步:访问根节点
  }
  dfs(root);
  return res;
}
// 输出:[4, 5, 2, 3, 1]

💡 递归的优缺点:代码简洁易理解,但递归本质是函数不断入栈,如果树的深度过大(比如超过1万层),会出现栈溢出错误,此时需要用迭代方式替代。


5.2 迭代遍历(用栈/队列模拟递归)

递归的本质是利用系统栈保存临时状态,我们可以手动用栈/队列模拟这个过程,避免栈溢出问题。

① 前序遍历(迭代版)

思路和递归一致,用栈保存待访问的节点,由于栈是后进先出,需要先压入右子节点,再压入左子节点,保证左子节点先被访问。

function preorderTraversalIterative(root) {
  if (!root) return [];
  const res = [];
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    res.push(node.val);
    // 先压右再压左,保证左先出栈
    if (node.right) stack.push(node.right);
    if (node.left) stack.push(node.left);
  }
  return res;
}
// 输出:[1, 2, 4, 5, 3]

② 中序遍历(迭代版)

需要先一路向左走到最深的左子节点,再回溯访问根节点,最后转向右子树。

function inorderTraversalIterative(root) {
  const res = [];
  const stack = [];
  let cur = root;
  while (cur || stack.length) {
    // 一路向左,把路径上的节点全部入栈
    while (cur) {
      stack.push(cur);
      cur = cur.left;
    }
    // 左到底了,弹出节点访问
    cur = stack.pop();
    res.push(cur.val);
    // 转向右子树
    cur = cur.right;
  }
  return res;
}
// 输出:[4, 2, 5, 1, 3]

③ 后序遍历(迭代版)

后序需要保证根节点在所有子节点之后访问,我们可以用「标记法」:给每个节点加一个是否已访问的标记,第一次入栈时不访问,等左右子树都遍历完后再访问根节点。

function postorderTraversalIterative(root) {
  const res = [];
  const stack = [];
  if (root) stack.push([root, false]); // [节点, 是否已访问]
  while (stack.length) {
    const [node, visited] = stack.pop();
    if (visited) {
      // 左右子树都已遍历完,访问根节点
      res.push(node.val);
    } else {
      // 先压入根节点(标记为已访问),保证最后弹出访问
      stack.push([node, true]);
      // 先压右子树,再压左子树,保证左先出栈
      if (node.right) stack.push([node.right, false]);
      if (node.left) stack.push([node.left, false]);
    }
  }
  return res;
}
// 输出:[4, 5, 2, 3, 1]

5.3 层序遍历(广度优先,迭代实现)

层序遍历是按「从上到下、从左到右」的顺序逐层访问节点,需要用到队列(先进先出)来保存每一层的节点:

function levelOrderTraversal(root) {
  const res = [];
  if (!root) return res;
  const queue = [root];
  while (queue.length) {
    const node = queue.shift();
    res.push(node.val);
    // 左子节点先入队,保证同一层从左到右访问
    if (node.left) queue.push(node.left);
    if (node.right) queue.push(node.right);
  }
  return res;
}
// 输出:[1, 2, 3, 4, 5]

六、总结与实践提示

  1. 二叉树是前端领域最常用的树形结构,后续的二叉搜索树、平衡二叉树、红黑树都是基于它衍生而来;
  2. 递归遍历适合树深度较小的场景,代码简洁;迭代遍历适合深度大的场景,避免栈溢出;
  3. 实际开发中,虚拟DOM Diff、路由权限树、菜单渲染等场景都会用到树的遍历逻辑,掌握基础后才能应对复杂场景。

你可以把上面的代码复制到浏览器控制台直接运行,修改示例二叉树的结构,观察不同遍历方式的输出变化,加深理解。