-树 Lecture 13 - Trees

官方信息技术老师·26 页·深入(追求细节与边界)·0 次浏览·2 天前
数据结构递归层级模型图论基础

Trees 树结构

看完你能从家族族谱出发,亲手画出树并讲清细节与边界

按 空格/→ 演示下一步

1 / 26 页

全部页面点击任意一页,跳回舞台从这页播放

数据结构递归层级模型图论基础

Trees 树结构

看完你能从家族族谱出发,亲手画出树并讲清细节与边界

1第 1 页 · Trees 树结构

什么是「树」这种结构

你打开公司组织架构图:从 CEO 往下,每一层都是「谁向谁汇报」的关系。把这种层层汇报画出来,就是一棵「树」。这页我们把它拆开看。

根节点
整棵树唯一的顶层入口,没有父节点
父节点到子节点的连线,承载层级关系
子树
任一节点及其所有后代,自身仍是完整一棵树
叶节点
度数为零的末端节点,不再向下延伸
公司组织架构图对应 →树结构

CEO 对应根;汇报关系对应边;基层员工对应叶节点

2第 2 页 · 什么是「树」这种结构

树的组成要素

中心是树的总称,外围每一支是一个组成要素;从最基础元素,到特殊节点,再到关系。

图解渲染中…
节点树中存储数据的基本单位节点之间的连接,承载父子关系整棵树的唯一起点,无父节点末端节点,没有子节点
3第 3 页 · 树的组成要素

树的递归定义

前面看了树的组成元素,但要严谨回答「什么是一棵树」,得靠递归——翻开一本书的目录:章下面有小节,小节下面还有小节,结构自己套着自己。

递归锚点
单个无子节点的节点,本身就是最小的树
根 + 子树
一棵树 = 一个根节点 + 若干棵子树
子树仍是树
子树内部仍按「根 + 更小子树」组织
递归必终止
递归一路展开,最末端落到叶子节点
公司组织架构对应 →树的递归结构

CEO 是根,部门是子树;每个部门内部仍有自己的头与小团队

T=r(T1,T2,,Tn)T = r(T_1, T_2, \ldots, T_n)
4第 4 页 · 树的递归定义

树 vs 链表 vs 图

链表是线、图是网、树是分支——三者都靠指针串联,但拓扑约束层层递进。

链表(线性)
  • 节点关系:每节点只有 1 个后继
  • 环路:无环,遍历必终止
  • 路径数:任意两节点至多 1 条
图(网络)
  • 节点关系:节点可有任意多个邻居
  • 环路:可有环,遍历需标记已访问
  • 路径数:两点之间可有多条路径
树是『无环 + 单父节点』的图:比链表多了分支,比图少了环路与多父。
5第 5 页 · 树 vs 链表 vs 图

深度优先搜索 DFS

DFS 像在迷宫里只带一根蜡烛:一直往前走,没路了才退回一步换条岔道。

1
从根节点出发
以根为唯一入口,初始化递归调用栈
2
访问当前节点
执行操作(打印/判断等),并标记为已访问
3
深入子节点
挑一个未访问的子节点递归进入,路径加长一格
4
撞墙后回溯
到达叶节点或子节点全部访问过,函数返回上一层
5
切换下一分支
回到父节点后选下一个未访问的子节点继续深入
6
遍历完成
没有未访问节点时,调用栈逐层弹出,结束
6第 6 页 · 深度优先搜索 DFS

广度优先搜索 BFS

BFS 按层推进,靠队列记住「下一层要访问的节点」。

1
根节点入队
起点:把根放入队列,作为第一层第一个待访问节点
2
出队并访问
从队列头部取出一个节点,处理它(记录/输出)
3
子节点全部入队
把当前节点的所有子节点依次加入队列尾部
4
循环至队列空
重复出队与入队,直到队列中再无节点
7第 7 页 · 广度优先搜索 BFS

DFS 与 BFS 对比图解

同一棵树两种遍历的路径差异

DFS 与 BFS 对比图解
同一棵树两种遍历的路径差异
8第 8 页 · DFS 与 BFS 对比图解

遍历实现代码

python

同一棵树用递归跑 DFS、用队列跑 BFS,看两种思路在代码里如何落地。

代码高亮加载中…

DFS 把『访问当前节点』与『对孩子递归』拆开写;BFS 用 popleft/append 保证先入先出,于是按层处理。

9第 9 页 · 遍历实现代码

前序 Pre-order 遍历

DFS 三种走法的区别,就在于「什么时候访问根节点」。前序遍历最直白:到了某个节点,先办它的事,再走左边,最后走右边。

访问顺序
根 → 左子树 → 右子树
递归骨架
visit(root) → preorder(left) → preorder(right)
为何适合复制
父节点先建好,子树拷完直接挂回去
迭代用栈
先压右再压左,弹出顺序就反过来
复制文件夹对应 →前序遍历

先建当前目录,再拷左、右子目录

10第 10 页 · 前序 Pre-order 遍历

中序 In-order 遍历

前序遍历先把根摆出来,再往左、右递归。中序只是把根挪到中间——左、根、右。但这一挪,在二叉搜索树上就变了:所有值会按从小到大自动输出。

顺序:左 → 根 → 右
先递归处理整棵左子树,再访问自身,最后递归处理整棵右子树
BST 经典输出
二叉搜索树用中序遍历,节点值自动按升序排列
递归实现
每个节点执行 inorder(左)、访问、inorder(右)
迭代需显式栈
不用递归时,用栈模拟:沿左链一路压入,再弹栈访问并转向右子树
图书馆按索书号走一圈对应 →BST 的中序遍历

书架按 BST 组织(左小右大),从最左走到最右 = 左→根→右 = 自动升序

11第 11 页 · 中序 In-order 遍历

后序 Post-order 遍历

中序遍历把根放中间,前序把根放最前面。后序反过来——把根放到最后:左右子树全处理完,才碰根。这种顺序有什么用?最直接的就是释放整棵树。

遍历顺序
左→右→根,子树全处理完才访问根
删树场景
必须先释放子节点,再释放父节点
递归特性
天然契合递归栈,子树先返回才处理当前节点
遍历对比
同属 DFS,根的访问时机决定语义
删除电脑文件夹对应 →后序遍历删树

必须先把里面所有文件删完,才能删这个文件夹本身

12第 12 页 · 后序 Post-order 遍历

三种顺序实战图解

同一棵树三种遍历的节点输出序列

三种顺序实战图解
同一棵树三种遍历的节点输出序列
13第 13 页 · 三种顺序实战图解

决策树是什么

每个内部节点是一个决策问题

决策树是什么
每个内部节点是一个决策问题
14第 14 页 · 决策树是什么

决策树示例:买不买咖啡

从根节点出发,每个判断节点根据条件分支,叶子节点就是最终决策。

图解渲染中…
A决策起点,代表一个待解决的问题B条件判断节点,用菱形表示,按条件分流D叶子节点,代表一个最终决定,不再分支G叶子节点,这里是买咖啡的具体方式
15第 15 页 · 决策树示例:买不买咖啡

决策边界与特征分裂

上一张我们用「天气+心情+钱」三个问题把买咖啡分成是和否。但问题是——三个特征先问哪个?为什么「天气」排在最前面?这一步的判断标准,就是决策树的核心。

分裂的难题
同一节点常有多个候选特征,先问哪个没有标准答案
信息增益
分裂后不确定性下降的幅度,越大说明分裂越有效
基尼不纯度
随机抽两样本类别不同的概率,越小节点越纯
贪心局部最优
每步只看当前最优分裂,不保证全局最优
轴平行边界
分裂总沿某个特征的阈值,形成矩形决策区域
医院分诊台对应 →特征分裂

导诊先量体温问病史,把最明显的急症先分流,剩下再细分科

H(S)=ipilog2piH(S) = -\sum_{i} p_i \log_2 p_i
16第 16 页 · 决策边界与特征分裂

决策树自测

点击作答

决策树在二维特征空间中做一次分裂,产生的边界是什么形状?

17第 17 页 · 决策树自测

隐式搜索问题

上一页我们用了买咖啡这样的现成决策树。但现实中,九宫格、华容道、魔方这类问题,摆在你面前的只是一个混乱局面——树在哪里?答案是:树要我们自己造。这叫「隐式搜索」。

隐式问题
问题本身不含树,需要我们去发现它
状态 State
某一时刻的完整快照,比如棋盘局面、当前路径
动作 Action
把一个状态合法地切换到另一个的操作
建模映射
状态当节点、动作当边,树就成型了
搜索即遍历
之后所有算法都在这棵人造树上跑
黑暗迷宫里探路对应 →隐式搜索建树

看不见全图,每走一步才照亮一段——树是边走边生成的

NbdN \approx b^d
18第 18 页 · 隐式搜索问题

回溯算法流程

回溯算法的精髓:选路→碰壁→退回→再选,反复循环直到找到目标。

1
从根出发
搜索从根节点开始,确保不遗漏任何分支
2
选分支向下
在当前节点做一次选择,沿对应子节点继续深入
3
判定可行性
检查是否到达目标,或已撞到死路/非法状态
4
回退到上层
若不可行,撤销本步选择,返回父节点
5
尝试下一支
在父节点选另一条未走过的分支,回到第3步判定
19第 19 页 · 回溯算法流程

回溯代码实现框架

python

用全排列示例展示回溯四要素:终止、剪枝、做选择、撤销。这是 Python 回溯的标准模板。

代码高亮加载中…

高亮行对应回溯四步:判定终止(L9)、剪枝跳过(L15)、做选择(L19)、递归(L20)、撤销选择(L21)。这就是决策树的深度优先遍历。

20第 20 页 · 回溯代码实现框架

树的复杂度问题

上一节回溯在隐式树上穷举,每层分若干支、层层叠加,听上去很自然。可一旦把分支数和深度乘起来,节点数就开始按指数级飙升——这是个没法忽视的代价。

分支因子与深度
节点数由 b(每层分支数)和 d(深度)共同决定
指数级增长
完整 b 叉深度 d 的树,节点数约为 b^d;d 加 1 节点数乘以 b
真实数字的冲击
b=10、d=20 时约 10^20 节点;每秒处理 10^9 个也需 3000 年
遍历的代价
完整遍历时间复杂度 O(b^d),深度微增代价暴涨
棋盘放米粒故事对应 →树的指数级节点增长

第 1 格 1 粒,每格翻倍;第 64 格 2^63 粒,全球年产米装不下

N(b,d)bd(完整 b 叉树,深度 d)N(b,d) \approx b^d \quad (\text{完整 }b\text{ 叉树,深度 }d)
21第 21 页 · 树的复杂度问题

剪枝策略

翻电话簿找'王'姓你不会从头翻到尾——A 到 Z 的顺序让你直接跳到后半。搜索树里的剪枝就是这种'提前跳步':判断某分支不会产出答案或最优解,就不再向下扩展。

剪枝本质
提前判定某分支无解或非最优,停止向下扩展
可行性剪枝
当前状态违反约束,子树整体非法
最优性剪枝
当前代价已≥已知最优,继续只会更差
对称性剪枝
多分支结构或中间状态等价,只搜一个代表
园丁剪枯枝对应 →搜索树剪枝

枯枝对应死分支;省下的养料对应省下的算力

22第 22 页 · 剪枝策略

记忆化搜索

剪枝让我们跳过不该走的分支,但还有一类浪费——同一个子树被反复算。比如递归求每个节点子树大小时,左右子树会被反复访问。记忆化搜索用一个「备忘录」存算过的结果,下次直接抄。

哈希表作备忘录
dict[key] → result,存子树形状或参数对应的答案
先查后算
进入递归前先查表,命中就直接返回不再递归
算完即记
算出新结果立刻写入 memo,下次遇到直接抄答案
背熟的数学公式对应 →记忆化搜索的备忘录

(a+b)² 展开式背下来,下次直接用,不重新推导

23第 23 页 · 记忆化搜索

普通递归 vs 记忆化

加了一行缓存,时间从指数降到线性——真的就改一行?

普通递归
  • 重复求解同一子问题
  • 时间 O(2ⁿ) 指数爆炸
  • 几乎不用额外空间
记忆化递归
  • 每个子问题只算一次
  • 时间 O(n) 线性可控
  • O(n) 空间换时间
子问题会重复出现时,用记忆化换时间;否则普通递归更简洁。
24第 24 页 · 普通递归 vs 记忆化

Trees 知识点总结

  • 树的本质是递归结构:每棵子树仍是树
  • DFS与BFS同为O(n),但适用场景不同
  • 前/中/后序决定根节点访问时机
  • 回溯是带约束的DFS,剪枝与记忆化能提速
  • 决策树把递归分裂落到特征空间,形成分类边界
延伸主题:二叉搜索树与平衡树堆与优先队列动态规划与树形DP
25第 25 页 · Trees 知识点总结

课后思考

先想再看答案。三个问题分别对应回顾、应用与边界,难度递进。

1树的定义为什么必须用递归?如果删掉「子树也是树」这一句会怎样?

参考答案因为树的每个节点都可以是一棵更小树的根——递归才能描述这种「自相似」。删掉这层,定义就停在根节点,无法表达分支结构。

2拿到一个新问题时,怎么判断该用前序、中序还是后序遍历?

参考答案看「根」的处理时机:前序=先处理根(自顶向下传信息);后序=最后处理根(自底向上汇总);中序=适用于有序二叉搜索树。

3如果树中每个节点的子节点数量不固定(如决策树),递归遍历还成立吗?为什么?

参考答案成立。把「左右子树」换成「子节点列表」,递归遍历每个子节点即可——结构本质未变,只是不再限制两个分支。

26第 26 页 · 课后思考
-树 Lecture 13 - Trees · 知识图解