PYTHON LESSON 089
考古学家复原古树
理解“前序定根、中序分左右”的重建原理,会用递归切片重建二叉树,再输出后序与层序验证。
00 · 学习目标
这一课要解决什么?
学完后,你应该能够
- pre[0] 就是当前子树的根
- ino.index(根) 把中序切成左右两段
- 递归切片:pre[1:k+1] 配 ino[:k]
01 · 核心概念
由前序与中序重建唯一的二叉树
由前序与中序重建唯一的二叉树:考级常给前序+中序求后序。硬背答案没前途,通用办法是先把树真正重建出来,之后想要什么遍历就遍历什么。先记住两个特点:前序的第一个元素必为根;中序里根左边的全在左子树、右边的全在右子树。
GESP Python 6 级 · 树的遍历GESP Python 6 级 · 树的定义与构造有向无环图、拓扑排序、强连通分量和负权最短路是图论后续主题。
02 · 语法与规则
先记住这 3 条,再开始写程序
pre[0] 就是当前子树的根先准确读出这条写法的结构与作用。
ino.index(根) 把中序切成左右两段换一组最小数据,手工推演一次结果。
递归切片:pre[1:k+1] 配 ino[:k]再用边界值或反例确认它的适用条件。
03 · 完整实例
代码、运行结果和解释放在一起看
# 考古学家复原古树:由前序和中序重建二叉树
from collections import deque
preorder = [3, 9, 20, 15, 7]
inorder = [9, 3, 15, 20, 7]
class Node:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def build(pre, ino):
if not pre:
return None
root_val = pre[0] # 前序第一个元素一定是根
k = ino.index(root_val) # 根在中序里的位置
root = Node(root_val)
root.left = build(pre[1:k + 1], ino[:k]) # 中序左边一段是左子树
root.right = build(pre[k + 1:], ino[k + 1:]) # 中序右边一段是右子树
return root
def postorder(node, bag):
if node is None:
return
postorder(node.left, bag)
postorder(node.right, bag)
bag.append(str(node.val))
root = build(preorder, inorder)
bag = []
postorder(root, bag)
print("前序:" + " ".join(map(str, preorder)))
print("中序:" + " ".join(map(str, inorder)))
print("重建后的后序:" + " ".join(bag))
queue = deque([root])
level = []
while queue:
node = queue.popleft()
level.append(str(node.val))
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
print("重建后的层序:" + " ".join(level))前序:3 9 20 15 7 中序:9 3 15 20 7 重建后的后序:9 15 7 20 3 重建后的层序:3 9 20 15 7
重建的关键是“前序给根、中序分段”,左右两段切片必须长度一致、互相对应。
04 · 逐步理解
每一步只解决一个问题
- 01
读题:两串序列还原一棵树
考级常给前序+中序求后序。硬背答案没前途,通用办法是先把树真正重建出来,之后想要什么遍历就遍历什么。先记住两个特点:前序的第一个元素必为根;中序里根左边的全在左子树、右边的全在右子树。
- 02
前序定根
pre[0] 就是当前这棵(子)树的根。用 ino.index(root_val) 找到它在中序里的位置 k:k 左边有 k 个节点属于左子树,右边剩下的是右子树。根找到了,左右阵营也就分开了。
root_val = pre[0] k = ino.index(root_val) - 03
中序分段,切片配对
左子树在中序里是 ino[:k],共 k 个节点;对应的前序切片是 pre[1:k+1]——跳过根,紧接着的 k 个就是左子树的前序。右子树同理:pre[k+1:] 配 ino[k+1:]。两段长度必须相等,这也是检查题目序列有没有抄错的第一道关。
root.left = build(pre[1:k + 1], ino[:k]) root.right = build(pre[k + 1:], ino[k + 1:]) - 04
递归的出口
切片每递归一次就小一圈,当某段为空(not pre)时,说明这个方向没有孩子,返回 None。空切片就是这台递归机器的刹车,必须先写。
if not pre: return None - 05
重建完再遍历
树已经在内存里站好了,后序遍历、层序遍历全是老套路:一个递归、一个队列。跑一遍和脑补的树形对照:层序是 3 9 20 15 7,说明 20 是根的右孩子、15 和 7 是它的两个孩子——全对。
- 06
为什么前序+后序不够
只有前序和后序时,根能确定,但分不清一棵“独生子”子树到底是左孩子还是右孩子,可能对应多棵不同的树。所以考级的标准搭配永远是“前序+中序”或“中序+后序”——中序负责分左右,缺它不行。
05 · 练习与检验
自己写出来,才算真正学会
- 1运行程序,核对重建出的后序与层序
- 2在中序里手指数一数根把序列切成了哪两段
- 3换一组前序、中序序列重建自己的树
- 4想一想:只知道前序和后序,能唯一重建吗
06 · 完整知识
继续理解定义、规则和适用边界
第一次学习先完成上面的六个步骤;需要查定义、核对规则、分析误区或理解“为什么”时,再展开对应知识章。
核心数据结构树、图、DFS、BFS 与最短路从节点与边的模型出发,掌握遍历、连通性、树结构、最小生成树和最短路径的适用条件。+
正式定义
图由顶点和边构成;树是连通且无环的无向图。DFS 沿路径深入后回溯,BFS 按距离层次扩展。算法的正确选择取决于图是否有向、边权是否为负、是否稠密以及目标是遍历、连通还是最短路。
必须掌握
- 邻接矩阵占 O(V²) 空间,适合稠密图和快速查边;邻接表占 O(V+E),适合稀疏图。
- DFS 常用递归或显式栈,BFS 使用队列;一般图都必须记录 visited 防止重复和死循环。
- 无权图的 BFS 首次到达即得到最少边数距离,可用 parent 还原路径。
- 树有 V-1 条边且任意两点路径唯一;二叉树前/中/后序描述根的访问时机。
- Dijkstra 只适用于非负边权;Floyd 求所有点对最短路并允许负边,但不能有可达负环。
- Kruskal 按边权排序并用并查集避环,得到连通无向带权图的最小生成树。
常见误区
- 遍历一般图时不记录 visited
- 对负权边使用 Dijkstra
- 混淆最短路径树与最小生成树
- 递归 DFS 忽略深度限制
适用边界
- 有向无环图、拓扑排序、强连通分量和负权最短路是图论后续主题。
- 网格搜索也是图搜索:格子是顶点,可移动关系是边;不要只背二维数组模板。
编码与文本字符串、转义、切片与格式化从不可变字符序列到检索、拆分、拼接、格式化和常用判断方法。+
正式定义
str 是不可变的 Unicode 字符序列。下标访问单个字符,切片生成新字符串;任何看似“修改字符串”的方法都会返回新对象。
必须掌握
- 下标从 0 开始,负下标从末尾开始;切片 s[start:stop:step] 不包含 stop,step 不能为 0。
- 转义序列用于在字面量中表示换行、制表、引号、反斜杠或码点;原始字符串仍有末尾反斜杠限制。
- find 找不到返回 -1,index 找不到抛 ValueError;count 统计不重叠出现次数。
- split 把字符串拆成列表,join 用一个字符串连接可迭代对象中的字符串,strip 只删除两端字符。
- f-string 的格式说明可控制宽度、对齐、精度、进制和百分比;格式化不改变原值。
- isalpha/isdigit 等按 Unicode 定义,不只识别英文字母和 ASCII 数字。
常见误区
- 尝试 s[0] = 'A' 原地修改字符串
- 把 strip('ab') 误解为删除完整子串 'ab'
- find 返回 -1 后直接拿去当有效下标
- 把字节长度与字符长度混为一谈
适用边界
- 正则表达式不在低等级字符串必修范围,但复杂模式匹配时应使用 re,而不是堆叠大量 split/find。
- 面向用户的字符计数还可能涉及组合字符和字素簇,len 统计的是 Unicode 码点序列长度。
程序组织模块、导入与常用标准库正确导入模块,并按用途查找 math、random、statistics、collections、heapq、bisect 等标准工具。+
正式定义
模块是可导入的 Python 代码单元,包把模块组织成层次。import 先加载模块并创建模块对象,再把名字绑定到当前命名空间;标准库随 Python 分发,不等于第三方包。
必须掌握
- import module 保留清晰命名空间;from module import name 只导入指定名字;避免 from module import *。
- math 提供 sqrt、floor、ceil、gcd、log、sin 等数学函数和 pi、e 等常量。
- random 用于伪随机模拟,不适合密码安全;需要安全随机时使用 secrets。
- collections.deque 适合双端队列,Counter 适合计数,defaultdict 可按工厂创建缺省值。
- heapq 实现最小堆,bisect 在已排序序列中二分定位,itertools 提供高效迭代组合工具。
- if __name__ == '__main__': 用来区分文件被直接运行还是被导入。
常见误区
- 文件名写成 math.py 导致遮蔽标准库
- import * 污染命名空间
- 把 random 当成加密随机数
- 没有理解工具的数据复杂度就盲目调用
适用边界
- 标准库非常大,本章覆盖课程会实际依赖的入口;具体函数签名应结合本站条目与运行时帮助查询。
- Turtle 需要图形窗口,本站文本判题环境不伪造其画布。
工程能力异常、文件、测试与调试读懂报错、缩小问题、设计测试,并安全地打开、读取和关闭文本文件。+
正式定义
异常是在运行期间表示错误或特殊情况的对象。调试是用可复现输入和证据定位实际行为与预期行为差异的过程;文件对象连接程序与持久化字节数据。
必须掌握
- 先读 traceback 最后一行的异常类型与消息,再从最靠近自己代码的栈帧向上追踪。
- try 只包可能失败的最小代码;except 捕获具体异常;else 处理成功路径;finally 做必需清理。
- raise 主动报告不满足的前置条件;assert 用于开发期内部假设,不用于校验不可信用户输入。
- with open(...) as file 会在退出代码块时可靠关闭文件。文本模式必须明确编码,本站统一推荐 encoding='utf-8'。
- 测试至少包含正常值、边界值、空数据、极端值和反例;每个测试只应有明确目的。
- 定位错误时一次只改一个假设,保留能稳定复现问题的最小输入。
常见误区
- 使用 except: 吞掉所有错误
- 只测题目样例就认为程序正确
- 文本文件不写 encoding
- 修复报错表象却不验证根因
适用边界
- 在线判题的学生代码由独立 Worker 执行;文件系统、网络和资源权限必须受平台限制。
- 二进制文件、JSON/CSV 和数据库各有专门格式与错误处理方式,不能按普通文本随意拆分。
完成检查