数据结构:二叉树及其遍历

50 阅读5分钟

前言

在前端开发和算法面试中,二叉树是最常见、最基础却又极易出错的数据结构之一。很多同学初学时觉得“简单”,但一到手写遍历代码或变种题目就卡壳。本文将从零开始,由浅入深地带你彻底掌握二叉树的核心概念、定义、遍历方式,并结合递归思维和常见面试考点,帮助你真正理解而不是死记硬背。

一、什么是树?为什么需要树?

现实世界中很多数据天然就具有层级关系:

  • 公司组织架构(CEO → 部门经理 → 员工)
  • 文件系统(文件夹嵌套)
  • DOM 树(HTML 元素嵌套)
  • 家族族谱

线性结构(数组、链表)无法高效表达这种“一对多”的关系,于是就有了树形结构。

树的核心特点:

  • 只有一个根节点(root)
  • 每个节点可以有多个子节点(children)
  • 没有环
  • 从根到任意节点有且仅有一条路径

树的相关术语

  • 节点(node):树的每一个元素
  • 边(edge):连接节点的线
  • 根节点(root):最顶层的节点
  • 叶子节点(leaf):没有子节点的节点(度为 0)
  • 节点的度(degree):该节点拥有的子树数量
  • 树的深度/高度:从根节点到最远叶子节点的最长路径上的节点数(或边数,视定义而定)
  • 层的概念:根节点为第 1 层,其子节点为第 2 层,以此类推

41103B80-1E00-4250-925D-5D5E24476F37.png

二、二叉树:最重要的一种树

二叉树(Binary Tree)是树家族中最常用的一种,每个节点最多只有两个子节点,分别称为左子节点(left)和右子节点(right)。

二叉树的严格定义(递归定义)

二叉树要用递归的方式来定义,这也是后面递归遍历的理论基础:

  1. 二叉树可以是空树(null)。
  2. 如果不是空树,则由三部分组成:根节点 + 左子树 + 右子树,且左子树、右子树本身也必须是合法的二叉树。

关键点:

  • 左右子树有严格顺序,不能互换(不同于普通树)
  • 每个节点的度最多为 2(不一定是正好 2,很多节点度为 1 或 0)

二叉树节点代码表示

JavaScript 中常见的两种写法:

// 类方式(更标准)
class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = null;
    this.right = null;
  }
}

// 对象字面量方式(适合快速构建测试用例)
const root = {
  val: "A",
  left: {
    val: "B",
    left: { val: "D", left: null, right: null },
    right: { val: "E", left: null, right: null }
  },
  right: {
    val: "C",
    right: { val: "F", left: null, right: null },
    left: null
  }
};

下面我们统一使用这棵树作为示例:

DD71B279-CF50-4759-AFEB-C9F233026936.png

三、二叉树的遍历方式(重点!)

二叉树遍历是面试中最经典的题目,考察你对递归和迭代的掌握程度。

共有四种主流遍历方式:

1. 深度优先遍历(DFS)—— 前序、中序、后序(递归最自然)

深度优先的特点:尽可能深地探索子树,到底后再回溯。

三种顺序的区别仅在于根节点访问的时机不同:

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

输出顺序:A B D E C F

function preorder(root) {
  if (!root) return;
  console.log(root.val);     // 先访问根
  preorder(root.left);       // 再左子树
  preorder(root.right);      // 最后右子树
}

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

输出顺序:D B E A C F (特别的:对二叉搜索树 BST,中序遍历结果是有序的!)

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

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

输出顺序:D E B F C A (应用场景:先处理子节点再处理父节点,如删除整个树、计算目录大小)

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

递归思维拆解

以 preorder 为例,递归本质:

preorder(A)
├── print A
├── preorder(B)
│   ├── print B
│   ├── preorder(D) → print D → return
│   └── preorder(E) → print E → return
└── preorder(C)
    ├── preorder(null)
    └── preorder(F) → print F → return

思考题:为什么递归写法如此简洁?因为二叉树的定义本身就是递归的,遍历问题天然就是“分治”问题:把大树的问题拆成左子树 + 右子树 + 根的处理。

2. 广度优先遍历(BFS)—— 层序遍历(Level Order)

广度优先的特点:一层一层地访问,常用队列实现。

输出:[[A], [B, C], [D, E, F]]

function levelOrder(root) {
  if (!root) return [];
  const result = [];
  const queue = [root];
  
  while (queue.length) {
    const levelSize = queue.length;
    const currentLevel = [];
    
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      currentLevel.push(node.val);
      
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    
    result.push(currentLevel);
  }
  
  return result;
}

四、面试高频考点延伸

掌握以上基础后,面试中常考变形题:

  1. 非递归实现三种深度遍历(用栈模拟递归)

    • 前序最简单,中序最难(需要记录访问状态)
  2. 给定前序 + 中序,能唯一恢复二叉树(经典构造题)

  3. 给定中序 + 后序,也能唯一恢复

  4. 给定前序 + 后序,大部分情况不能唯一恢复(特殊情况除外)

  5. 层序遍历的变种:之字形打印(奇偶层反向)、每一层返回最后一个节点等

  6. 二叉树的最大深度、最小深度、对称性判断、路径和等问题(递归分治思想)

  7. Morris 遍历(O(1) 空间的线程二叉树遍历,较冷门)

五、总结与建议

二叉树遍历的核心在于:

  • 理解递归定义 → 自然写出递归遍历
  • 记住根节点访问的相对顺序 → 区分前/中/后序
  • 队列 vs 栈 → 层序 vs 深度优先

学习建议:

  1. 手画一棵小树,亲自走一遍四种遍历,观察输出顺序。
  2. 尝试把递归改成迭代(栈),体会系统栈与显式栈的对应。
  3. LeetCode 上刷经典题:94(中序)、144(前序)、145(后序)、102(层序)、101(对称二叉树)、226(翻转二叉树)。

二叉树是递归思维的最佳入门载体,掌握它,你会发现后续的回溯、分治、动态规划都变得顺畅很多。