从树到楼梯:数据结构与算法的奇妙旅程

引言

在编程学习中,有些概念初看抽象难懂,但一旦理解,便如打开新世界的大门。今天,我们将用最通俗易懂的方式,深入讲解三个经典主题:

  1. 树与二叉树 ------ 层级数据的王者;
  2. 列表转树 ------ 扁平数据如何变成立体结构;
  3. 爬楼梯问题 ------ 递归、记忆化与迭代的实战演练。

我们将根据实际代码,并逐行解释其原理。无论你是刚入门的新手,还是正在准备面试的开发者,这篇文章都将为你提供清晰、系统、完整的知识图谱。


第一部分:树与二叉树 ------ 数据的层级之美 🌳

1.1 什么是"树"?

自然界中的树有根、干、枝、叶。在计算机科学中,"树"是一种非线性 的数据结构,用于表示具有层级关系的数据。

"一棵树只有一个树根,向上伸展出无数的树支(非线性)。树枝上会有树叶。数据结构中,树是对自然界树的一种抽象。"

树的核心概念:
术语 含义
根节点(Root) 整棵树的起点,没有父节点。
节点(Node) 树的基本单元,可以包含数据和指向子节点的引用。
叶子节点(Leaf) 没有子节点的节点。
边(Branch/Edge) 连接两个节点的线。
层次(Level) 根为第1层,往下依次+1。
高度(Height) 叶子节点高度为0,往上依次+1。
深度(Depth) 节点到根的距离(根深度为0或1,依定义而定)。
度(Degree) 一个节点拥有的子节点数量。叶子节点度为0。

✅ 树的本质:一对多的关系结构。


1.2 什么是二叉树?

二叉树是树的一种特殊形式,具有严格规则:

"如果它不是空树,那么必须有根节点、左子树、右子树组成。且左右子树都是二叉树。左右子树有左右之分,是有顺序的。每个节点最多只有两个子节点。"

关键点:

  • 最多两个孩子:左子树 + 右子树。
  • 顺序不可交换:左 ≠ 右。
  • 可以为空树 (即 null)。
  • 不是所有节点度都为2!有些节点可能只有左孩子,或只有右孩子,甚至没有孩子。

❌ 常见误解:二叉树 ≠ 度为2的树。

✅ 正确认知:二叉树强调结构顺序,而非度数。


1.3 二叉树的节点定义

javascript 复制代码
function TreeNode(val) {
  this.val = val // 从右到左的执行顺序
  this.left = this.right = null
}

或者用对象字面量(更直观):

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

💡 在 JavaScript 中,树通常用嵌套对象 表示,每个节点有 valleftright 三个属性。


1.4 二叉树的遍历方式

遍历 = 访问树中每一个节点。根据访问根节点的时机不同 ,分为三种深度优先遍历(DFS)

(1)前序遍历(Preorder):根 → 左 → 右
javascript 复制代码
function preorder(root) {
  if (!root) { return };
  console.log(root.val);
  preorder(root.left);
  preorder(root.right);
}

执行逻辑

  1. 如果当前节点为空,直接返回(递归终止条件)。
  2. 先打印当前节点值(访问根)。
  3. 递归遍历左子树。
  4. 递归遍历右子树。

✅ 用途:复制树、序列化树、打印目录结构(先显示父目录)。


(2)中序遍历(Inorder):左 → 根 → 右
javascript 复制代码
function inorder(root) {
  if (!root) { return }
  inorder(root.left);
  console.log(root.val);
  inorder(root.right);
}

执行逻辑

  1. 先递归遍历左子树。
  2. 再访问当前节点。
  3. 最后遍历右子树。

✅ 特别重要:对二叉搜索树(BST) ,中序遍历结果是升序排列


(3)后序遍历(Postorder):左 → 右 → 根
javascript 复制代码
function postorder(root) {
  if (!root) { return }
  postorder(root.left);
  postorder(root.right);
  console.log(root.val);
}

执行逻辑

  1. 先处理左子树。
  2. 再处理右子树。
  3. 最后处理当前节点。

✅ 用途:删除树(先删孩子再删自己)、计算目录总大小(先算子目录再汇总)。


三者共同点与区别
遍历方式 根访问顺序 左右顺序 典型用途
前序 最先 左→右 复制、打印
中序 中间 左→右 排序(BST)
后序 最后 左→右 删除、汇总

⚠️ 注意:左右子树的访问顺序永远是"先左后右",这是约定俗成的。


1.5 层序遍历(BFS)------ 用队列实现

不同于上述"深度优先",层序遍历(Level Order) 是"广度优先":从上到下、从左到右一层层访问。

这需要借助队列(Queue) ------ 先进先出(FIFO)的数据结构。

javascript 复制代码
// 层序遍历 level order traversal 树的层级从上到下
// 每一层从左到右遍历 迭代 借助队列
class TreeNode {
  constructor(val) {
    this.val = val
    this.left = this.right = null
  }
}

function levelOrder(root) {
  if (!root) { return [] }
  const queue = [root]
  const res = []
  while (queue.length) {
    // 迭代 每一次循环都是一层
    const node = queue.shift() // shift 出队 先进先出
    res.push(node.val) // 每一层的节点值 放到 res 中
    if (node.left) { queue.push(node.left) }
    if (node.right) { queue.push(node.right) }
  }
  return res
}

逐行解析

  1. 若根为空,返回空数组。
  2. 初始化队列,将根节点入队。
  3. 循环直到队列为空:
    • 出队一个节点(shift())。
    • 将其值加入结果数组。
    • 将其非空的左右孩子依次入队。
  4. 返回结果。

示例:对如下树

复制代码
    A
   / \
  B   C
 / \   \
D   E   F

层序结果为:['A', 'B', 'C', 'D', 'E', 'F']

💡 层序遍历常用于:按层级展示数据、求树的宽度、序列化/反序列化树。


第二部分:列表转树 ------ 扁平数据的立体重生

2.1 问题背景

数据库或 API 经常返回这样的扁平列表

复制代码
[
  { id: 1, name: '中国', parentId: 0 },
  { id: 2, name: '北京', parentId: 1 },
  { id: 3, name: '上海', parentId: 1 },
  { id: 4, name: '东城区', parentId: 2 }
]

其中:

  • parentId === 0 表示根节点。
  • 其他节点通过 parentId 指向父节点。

我们的目标:将其转换为树形结构 ,每个节点包含 children 数组。


2.2 方法一:递归法

javascript 复制代码
// 扁平列表
// parentId 树状结构
// 树根 parentId 为 0
// 树结构 -> 递归实现
const list = [
  { id: 1, name: 'A', parentId: 0 },
  { id: 2, name: 'B', parentId: 1 },
  { id: 3, name: 'C', parentId: 2 },
  { id: 4, name: 'D', parentId: 3 },
]

// 递归
// 递归公式 分析重复的事
// 退出条件
function list2tree(list, parentId = 0) {
  const result = []
  list.forEach(item => {
    // 外层循环
    if (item.parentId === parentId) {
      const children = list2tree(list, item.id)
      if (children.length) {
        item.children = children
      }
      result.push(item)
    }
  })
  return result
}

// 优化版 es6语法
// 时间复杂度 O(n)
// 空间复杂度 O(n)
function list2tree(list, parentId = 0) {
  return list
    .filter(item => item.parentId === parentId)
    .map(item => {
      return {
        ...item,
        children: list2tree(list, item.id)
      }
    })
}

console.log(list2tree(list))

原理

  • 对每个 parentId,找出所有直接子节点。
  • 对每个子节点,递归构建它的子树。
  • 使用 filter + map 实现函数式风格,代码简洁。

⚠️ 缺点 :每次递归都要遍历整个列表,时间复杂度为 O(n²)(n 为节点数)。


2.3 方法二:Map 优化法

javascript 复制代码
function listToTree(list) {
  const nodeMap = new Map();
  const tree = [];
  list.forEach(item => {
    nodeMap.set(item.id, { ...item, children: [] })
  })
  list.forEach(item => {
    if (item.parentId === 0) {
      tree.push(item)
    } else {
      const parentNode = nodeMap.get(item.parentId)
      if (parentNode) {
        parentNode.children.push(item)
      }
    }
  })
  return tree;
}

步骤详解

  1. 第一遍遍历 :将所有节点存入 Map,以 id 为键,值为 {...item, children: []}
    • 这样后续可以通过 id O(1) 查找任意节点。
  2. 第二遍遍历
    • parentId === 0,说明是根节点,加入 tree
    • 否则,从 Map 中取出父节点,将当前节点 push 到其 children 中。

优点

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
  • 无递归,不会栈溢出。

💡 这是工程中最推荐的做法!


2.4 实际应用场景

  • 省市区三级联动选择器
  • 后台管理系统的菜单树
  • 文件资源管理器的目录结构
  • 评论系统的嵌套回复(如 Reddit、知乎)
  • 组织架构图

面试高频题!务必掌握两种解法。


第三部分:爬楼梯 ------ 递归的艺术与优化

3.1 问题描述(LeetCode #70)

力扣题链接:70. 爬楼梯 - 力扣(LeetCode)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。

问:有多少种不同的方法可以爬到楼顶?

分析:
  • 到第 n 阶,只能从 n-1(走1步)或 n-2(走2步)来。
  • 所以:f(n) = f(n-1) + f(n-2)
  • 边界条件:f(1)=1, f(2)=2

这正是斐波那契数列


3.2 方法一:朴素递归

javascript 复制代码
function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  return climbStairs(n-1) + climbStairs(n-2);
}

问题

  • 大量重复计算 !例如 f(5) 会多次计算 f(3)
  • 时间复杂度:O(2ⁿ) ------ 指数爆炸!
  • 调用栈过深可能导致爆栈(Stack Overflow)。

❌ 不可用于实际项目。


3.3 方法二:记忆化递归(Memoization)

javascript 复制代码
// 太多的重复计算 调用栈的内存的爆栈
// 空间换时间
const memo = {};
function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  if (memo[n]) return memo[n];
  memo[n] = climbStairs(n-1) + climbStairs(n-2);
  return memo[n];
}

改进

  • memo 缓存已计算结果。
  • 每个 f(n) 只计算一次。
  • 时间复杂度:O(n) ,空间:O(n)

⚠️ 但 memo 是全局变量,多次调用会污染(比如先算 f(10),再算 f(5),缓存还在)。


3.4 方法三:闭包私有化缓存(优雅解法)

javascript 复制代码
// 外层函数创建闭包环境,私有化memo
const climbStairs = (function() {
  // 缓存对象,被闭包持有,多次调用不会重置
  const memo = {};
  // 内层函数实现核心逻辑,访问外层的memo
  return function climb(n) {
    // 边界条件
    if (n === 1) return 1;
    if (n === 2) return 2;
    // 优先读取缓存,避免重复递归计算
    if (memo[n]) return memo[n];
    // 递归计算并缓存结果
    memo[n] = climb(n - 1) + climb(n - 2);
    return memo[n];
  };
})();

// 测试示例
console.log(climbStairs(3)); // 输出 3
console.log(climbStairs(5)); // 输出 8
console.log(climbStairs(10)); // 输出 55

优点

  • memo 被闭包保护,私有且持久
  • 多次调用 climbStairs 会复用缓存,效率高。
  • 代码模块化,无全局污染。

✅ 适合需要多次调用的场景。


3.5 方法四:自底向上迭代

javascript 复制代码
// 自底向上思考
// 迭代实现
// f(1) = 1;
// f(2) = 2;
// f(3) = f(2) + f(1);
// f(4) = f(3) + f(2);
// f(5) = f(4) + f(3);
function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  let f1 = 1;
  let f2 = 2;
  let f3 = 0;
  for (let i = 3; i <= n; i++) {
    f3 = f2 + f1;
    f1 = f2;
    f2 = f3;
  }
  return f3;
}

思路

  • f(1)f(2) 开始,逐步计算到 f(n)
  • 只需保存前两个值,无需数组或递归。

时间复杂度:O(n),空间复杂度:O(1)

✅ 无递归,无爆栈风险

✅ 最适合生产环境


总结:思维的升华

主题 核心思想 关键技巧
树与遍历 递归分解问题 前/中/后序(DFS),层序(BFS + 队列)
列表转树 构建层级关系 递归(O(n²)) vs Map 优化(O(n))
爬楼梯 动态规划雏形 朴素递归 → 记忆化 → 迭代优化

这些题目看似独立,实则共享同一套思维模式:

  • 递归:将大问题拆为相似的小问题。
  • 优化:用空间换时间(缓存、Map)。
  • 迭代:自底向上,避免递归开销。

编程不仅是写代码,更是建模现实、优化思维的过程。

希望这篇超详细指南,能让你彻底掌握这些经典问题。下次遇到树、列表转树、动态规划题时,你将胸有成竹,从容应对!

相关推荐
d111111111d2 小时前
STM32-HAL库学习,初识HAL库
笔记·stm32·单片机·嵌入式硬件·学习
Salt_07282 小时前
DAY 41 Dataset 和 Dataloader 类
python·算法·机器学习
BD_Marathon2 小时前
Vue3组件(SFC)拼接页面
前端·javascript·vue.js
长安er2 小时前
LeetCode 124/543 树形DP
算法·leetcode·二叉树·动态规划·回溯
wregjru2 小时前
【C++】2.3 二叉搜索树的实现(附代码)
开发语言·前端·javascript
Hao_Harrision2 小时前
50天50个小项目 (React19 + Tailwindcss V4) ✨ | StickyNavbar(粘性导航栏)
前端·typescript·react·tailwindcss·vite7
Sheep Shaun2 小时前
STL:list,stack和queue
数据结构·c++·算法·链表·list
杜子不疼.2 小时前
【LeetCode 153 & 173_二分查找】寻找旋转排序数组中的最小值 & 缺失的数字
算法·leetcode·职场和发展
头疼的程序员2 小时前
计算机网络:自顶向下方法(第七版)第一章 学习分享
网络·学习·计算机网络