图解树与二叉树:从概念到遍历,初学者也能轻松拿捏的算法基础🔥

68 阅读13分钟

在数据结构的世界里,树是一种极具 “自然美感” 的非线性结构 —— 它不像数组、链表那样 “一条道走到黑”,而是像自然界的树一样,从根到枝、从枝到叶层层延展。作为算法面试的高频考点,树与二叉树不仅是基础数据结构,更是理解红黑树、B 树、堆等高级结构的钥匙。

本文将结合通俗比喻、可视化图解和逐行拆解的代码,从 “是什么” 到 “怎么用”,带初学者彻底吃透树与二叉树的核心知识点。无论你是刚入门编程的新手,还是想巩固基础的开发者,都能在这里找到清晰的学习路径~


一、树:非线性结构的 “自然抽象”

1. 从自然界到代码:树的核心映射

你一定见过自然界的树 —— 一根树干向上生长,分出无数树枝,树枝末端长着树叶。数据结构中的 “树”,正是对这种形态的抽象:

自然界的树数据结构中的树核心作用
树根根节点(root)树的起点,唯一没有 “父节点” 的节点
树枝边(edge)连接两个节点,代表节点间的关联关系
树枝分叉处非叶子节点能延伸出子节点的 “中间节点”
树叶叶子节点(leaf)没有子节点的 “终点节点”
整棵树树(Tree)由 n≥0 个节点和 n-1 条边组成的非线性结构(n=0 时为空树)

关键特性:树中没有 “环”(类似树枝不会自己绕回树干),任意两个节点之间有且只有一条路径 —— 这是树与图的核心区别。

2. 树的核心概念:一次搞懂不混淆

初学者最容易在 “层次、高度、深度” 这些术语上迷路,这里用 “公司组织架构” 帮你直观理解:

plaintext

        董事长(根节点)—— 第1层
       /        \
  部门经理A    部门经理B —— 第2层
  /    \          \
员工1  员工2      员工3 —— 第3
  • 层次(Level) :从根节点开始计数,根节点是第 1 层(部分教材记为第 0 层,需注意场景),其子节点为第 2 层,依次递增。
  • 深度(Depth) :从根节点到当前节点的 “路径长度”(节点数)。比如员工 1 的深度是 3(董事长→经理 A→员工 1)。
  • 高度(Height) :从当前节点到最远叶子节点的 “路径长度”(节点数)。比如部门经理 A 的高度是 2(经理 A→员工 1),叶子节点的高度永远是 1。
  • 度(Degree) :节点的 “子节点个数”。董事长的度是 2(管理 2 个经理),员工 1 的度是 0(无下属),树的度是所有节点度的最大值(此例为 2)。
  • 叶子节点:度为 0 的节点(无子女),比如员工 1、员工 2、员工 3。

💡 记忆技巧:深度 “从根往下算”,高度 “从叶往上算”,层次是节点的 “所在楼层”。


二、二叉树:树的 “简化版王者”

二叉树是树中最常用的类型,它的 “简化规则” 让实现和算法变得异常高效 ——每个节点最多有两个子节点,且左、右子树顺序固定

1. 二叉树的定义:递归是核心

二叉树的定义本身就充满了递归思想(这也是树的核心魅力):

  • 二叉树可以是空树(没有任何节点);
  • 若不为空,则由根节点左子树右子树组成,且左、右子树也必须是二叉树;
  • 左子树和右子树有严格顺序,不能随意交换(比如 “左子树是 2,右子树是 3” 和 “左子树是 3,右子树是 2” 是两棵不同的二叉树)。

2. 关键误区:二叉树≠度为 2 的树

很多初学者会误以为 “二叉树就是每个节点度最多为 2 的树”,但这只说对了一半!核心区别在于:

  • 度为 2 的树:不允许存在 “单个子节点”(比如一个节点只能有 0 或 2 个子节点,不能只有 1 个),且无左右顺序;
  • 二叉树:允许存在 “单个子节点”(比如只有左子树或只有右子树),且左右顺序严格(左子树≠右子树)。

举个例子:一个节点只有右子树,这是合法的二叉树,但不是度为 2 的树。

3. 二叉树的节点结构:代码如何表示?

二叉树的节点需要存储 “数据” 和 “子节点引用”,在 JavaScript 中主要有 3 种表示方式,各有优劣:

方式 1:函数式构造函数(传统经典)

javascript

运行

// 定义二叉树节点构造函数
function TreeNode(val) {
  this.val = val; // 数据域:存储节点值
  this.left = this.right = null; // 引用域:左、右子节点初始化为null
}

// 构建一棵二叉树(数字版)
const root = new TreeNode(1); // 根节点
const node2 = new TreeNode(2); // 左子节点
const node3 = new TreeNode(3); // 右子节点
root.left = node2; // 根节点的左子树指向node2
root.right = node3; // 根节点的右子树指向node3

const node4 = new TreeNode(1); // node2的左子节点
const node5 = new TreeNode(1); // node2的右子节点
node2.left = node4;
node2.right = node5;

逐行解读

  • function TreeNode(val):定义构造函数,参数val是节点的数据;
  • this.val = val:给节点绑定数据域,存储具体值;
  • this.left = this.right = null:初始化左、右子节点引用为null(新节点默认没有子节点);
  • 后续通过root.left = node2这种赋值,建立节点间的关联,形成树结构。

方式 2:ES6 Class(语义清晰)

javascript

运行

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

// 构建字符版二叉树(后续遍历用)
const root = new TreeNode('A');
root.left = new TreeNode('B');
root.right = new TreeNode('C');
root.left.left = new TreeNode('D');
root.left.right = new TreeNode('E');
root.right.right = new TreeNode('F');

优势:符合现代 JavaScript 语法,语义更清晰,适合需要扩展节点方法(如isLeaf()判断是否为叶子节点)的场景。

方式 3:对象字面量(直观简洁)

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

优势:无需先定义构造函数,直接通过对象嵌套表示树结构,适合快速编写测试用例或临时树结构。

💡 实战建议:日常开发 / 面试中,对象字面量适合快速演示,Class 适合动态构建树(如插入、删除节点)。


三、二叉树的遍历:算法的 “核心操作”

遍历是二叉树最基础也最重要的操作 —— 指 “按一定顺序访问所有节点,且每个节点仅访问一次”。根据访问顺序的不同,分为深度优先遍历(DFS)  和广度优先遍历(BFS)  两大类,其中 DFS 又细分为前序、中序、后序遍历。

1. 先搞懂:为什么遍历需要 “递归”?

树的结构是递归定义的(一棵大树由无数棵小树组成),因此遍历树的最优方式就是递归 —— 把 “遍历整棵树” 拆分为 “遍历根节点 + 遍历左子树 + 遍历右子树”,直到遇到空节点。

递归遍历的三要素(必须牢记):

  • 终止条件:遇到null节点(没有子节点可遍历),直接返回;
  • 递推关系:遍历当前节点 → 遍历左子树 → 遍历右子树(或其他顺序);
  • 返回值:根据需求返回(如收集节点值的数组、节点个数等)。

2. 深度优先遍历(DFS):“钻到底再回头”

深度优先遍历就像 “走迷宫”—— 沿着一条路径走到尽头(叶子节点),再回溯到上一个分叉口,走另一条路径。核心是用 “栈” 存储待访问节点(递归时隐式使用系统栈,迭代时手动用栈)。

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

遍历顺序:先访问当前节点,再递归遍历左子树,最后递归遍历右子树。

javascript

运行

// 递归版前序遍历(返回节点值数组,方便后续复用)
function preorder(root) {
  const result = []; // 存储遍历结果

  // 内部递归函数:处理单个节点的遍历
  function traverse(node) {
    if (!node) return; // 终止条件:空节点直接返回
    result.push(node.val); // 1. 访问当前节点(核心:根节点先入队)
    traverse(node.left); // 2. 递归遍历左子树
    traverse(node.right); // 3. 递归遍历右子树
  }

  traverse(root); // 从根节点开始遍历
  return result;
}

// 测试:用字符版树(root = { val: 'A', left: ... })
console.log(preorder(root)); // 输出:["A", "B", "D", "E", "C", "F"]

逐行解读

  • const result = []:创建数组存储遍历结果(避免直接打印,提高复用性);
  • function traverse(node):内部递归函数,负责处理单个节点的遍历逻辑;
  • if (!node) return:终止条件,空节点无需处理;
  • result.push(node.val):访问当前节点,将值存入结果数组;
  • traverse(node.left):递归遍历左子树(先钻到最左叶子节点);
  • traverse(node.right):左子树遍历完后,递归遍历右子树。

可视化流程(以字符树为例):

  1. 访问根节点A → 结果:[A]
  2. 遍历A的左子树B → 访问B → 结果:[A, B]
  3. 遍历B的左子树D → 访问D → 结果:[A, B, D]
  4. D无左 / 右子树,回溯到B → 遍历B的右子树E → 访问E → 结果:[A, B, D, E]
  5. E无左 / 右子树,回溯到A → 遍历A的右子树C → 访问C → 结果:[A, B, D, E, C]
  6. 遍历C的右子树F → 访问F → 结果:[A, B, D, E, C, F]

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

遍历顺序:先递归遍历左子树,再访问当前节点,最后递归遍历右子树(二叉搜索树的中序遍历是有序的,这是核心考点)。

javascript

运行

function inorder(root) {
  const result = [];

  function traverse(node) {
    if (!node) return;
    traverse(node.left); // 1. 先遍历左子树(钻到最左)
    result.push(node.val); // 2. 访问当前节点(核心:左子树遍历完再访问根)
    traverse(node.right); // 3. 遍历右子树
  }

  traverse(root);
  return result;
}

console.log(inorder(root)); // 输出:["D", "B", "E", "A", "C", "F"]

关键提醒:中序遍历的核心是 “左子树优先”,比如遍历B节点时,必须先遍历完DB的左子树),才会访问B

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

遍历顺序:先递归遍历左子树,再递归遍历右子树,最后访问当前节点。

javascript

运行

function postorder(root) {
  const result = [];

  function traverse(node) {
    if (!node) return;
    traverse(node.left); // 1. 遍历左子树
    traverse(node.right); // 2. 遍历右子树
    result.push(node.val); // 3. 最后访问当前节点(核心:左右都遍历完才访问根)
  }

  traverse(root);
  return result;
}

console.log(postorder(root)); // 输出:["D", "E", "B", "F", "C", "A"]

记忆技巧:前、中、后序的区别仅在于 “访问根节点的时机”—— 前(根先)、中(根中)、后(根后),而左子树永远在右子树之前遍历。

3. 广度优先遍历(BFS):“逐层扫描不回头”

广度优先遍历就像 “从上到下扫楼”—— 先访问根节点(第 1 层),再访问第 2 层所有节点,接着第 3 层,直到所有层遍历完。核心是用 “队列” 存储待访问节点(队列先进先出,保证层级顺序)。

层序遍历:按层访问(面试高频)

javascript

运行

// 层序遍历(返回一维数组:所有节点按层从左到右)
function levelOrder(root) {
  if (!root) return []; // 空树直接返回空数组
  const result = []; // 存储遍历结果
  const queue = [root]; // 队列:初始化存入根节点

  // 队列不为空,说明还有节点待访问
  while (queue.length) {
    const node = queue.shift(); // 1. 队首节点出队(先进先出)
    result.push(node.val); // 2. 访问当前节点

    // 3. 左、右子节点入队(下一层节点)
    if (node.left) queue.push(node.left);
    if (node.right) queue.push(node.right);
  }

  return result;
}

// 测试:字符版树
console.log(levelOrder(root)); // 输出:["A", "B", "C", "D", "E", "F"]

逐行解读

  • if (!root) return []:空树处理,避免后续报错;
  • const queue = [root]:队列初始化,根节点是第一个待访问节点;
  • while (queue.length):队列不为空时,循环处理节点;
  • const node = queue.shift():队首节点出队(队列先进先出,保证按层访问);
  • result.push(node.val):访问当前节点,存入结果;
  • node.left && queue.push(node.left):左子节点入队(下一层的左节点先入队,保证从左到右);
  • node.right && queue.push(node.right):右子节点入队。

进阶:按层分组输出(面试常考变形)

如果要求返回 “按层分组的数组”(如[[A], [B,C], [D,E,F]]),只需记录每层的节点数:

javascript

运行

function levelOrderGrouped(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;
}

console.log(levelOrderGrouped(root)); // 输出:[["A"], ["B", "C"], ["D", "E", "F"]]

4. 递归 vs 迭代:什么时候该用哪种?

遍历方式递归版迭代版
优点代码简洁、逻辑清晰,符合树的递归本质无栈溢出风险(递归深度过大时会触发栈溢出),性能更稳定
缺点递归深度过大(如 10000 层树)会触发栈溢出代码稍复杂,需要手动管理栈 / 队列
适用场景树的深度较小,追求代码简洁树的深度较大(如大型数据结构),要求稳定性

💡 面试建议:先写出递归版(快速得分),再补充迭代版(展示功底),面试官会更认可~


四、作者私藏:初学者避坑指南

1. 常见误区纠正

  • ❌ 误区 1:二叉树是度为 2 的树 → ✅ 正确:二叉树允许单个子节点,且左右顺序严格;
  • ❌ 误区 2:递归遍历只能打印节点 → ✅ 正确:应返回结果数组(如result),提高代码复用性;
  • ❌ 误区 3:层序遍历只能用递归 → ✅ 正确:层序遍历本质是 BFS,必须用队列(递归不适合);
  • ❌ 误区 4:leftright可以随意交换 → ✅ 正确:二叉树的左右子树顺序固定,交换后是不同的树。

2. 记忆口诀(背会就能写代码)

  • 前序遍历:根左右,先存根;
  • 中序遍历:左根右,左完存根;
  • 后序遍历:左右根,左右完存根;
  • 层序遍历:队列存,先进先出。

3. 实战小练习(巩固所学)

给定如下二叉树,写出前序、中序、后序、层序遍历的结果:

javascript

运行

const testTree = {
  val: 5,
  left: { val: 3, left: { val: 2 }, right: { val: 4 } },
  right: { val: 7, right: { val: 8 } }
};

答案:

  • 前序:[5, 3, 2, 4, 7, 8]
  • 中序:[2, 3, 4, 5, 7, 8]
  • 后序:[2, 4, 3, 8, 7, 5]
  • 层序:[5, 3, 7, 2, 4, 8]

五、总结:从基础到进阶的路径

树与二叉树的学习核心是 “理解递归本质 + 掌握遍历逻辑”:

  1. 入门:搞懂树的核心概念(层次、高度、深度)和二叉树的定义;
  2. 基础:掌握节点的三种表示方式,能手动构建二叉树;
  3. 核心:熟练写出前序、中序、后序(递归 + 迭代)和层序遍历;
  4. 进阶:学习二叉搜索树(BST)、平衡二叉树(AVL)、堆等高级结构(均基于二叉树扩展)。

如果本文对你有帮助,不妨动手敲一遍代码 —— 数据结构的学习,“实践” 永远是最好的老师。后续我会继续分享二叉搜索树、树的修改(插入 / 删除)等进阶内容,关注我,一起从算法小白成长为高手~

最后,祝大家在算法的世界里,既能 “钻得深”(DFS),也能 “看得远”(BFS)!🚀