深入建议 50 分钟学习等级 6/8

PYTHON LESSON 089

考古学家复原古树

理解“前序定根、中序分左右”的重建原理,会用递归切片重建二叉树,再输出后序与层序验证。

00 · 学习目标

这一课要解决什么?

先想一想考古队只挖到前序和中序两串刻痕,能把整棵古树的模样复原出来吗?
完成任务古树结构复原仪
学习顺序正式定义 → 机制推演 → 边界与反例 → 迁移

学完后,你应该能够

  • pre[0] 就是当前子树的根
  • ino.index(根) 把中序切成左右两段
  • 递归切片:pre[1:k+1] 配 ino[:k]

01 · 核心概念

由前序与中序重建唯一的二叉树

由前序与中序重建唯一的二叉树:考级常给前序+中序求后序。硬背答案没前途,通用办法是先把树真正重建出来,之后想要什么遍历就遍历什么。先记住两个特点:前序的第一个元素必为根;中序里根左边的全在左子树、右边的全在右子树。

考级对应GESP Python 6 级 · 树的遍历GESP Python 6 级 · 树的定义与构造
学习边界

有向无环图、拓扑排序、强连通分量和负权最短路是图论后续主题。

02 · 语法与规则

先记住这 3 条,再开始写程序

01pre[0] 就是当前子树的根

先准确读出这条写法的结构与作用。

02ino.index(根) 把中序切成左右两段

换一组最小数据,手工推演一次结果。

03递归切片:pre[1:k+1] 配 ino[:k]

再用边界值或反例确认它的适用条件。

03 · 完整实例

代码、运行结果和解释放在一起看

g6-tree-rebuild.pyPYTHON 3.12
# 考古学家复原古树:由前序和中序重建二叉树
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))
运行结果OUTPUT
前序:3 9 20 15 7
中序:9 3 15 20 7
重建后的后序:9 15 7 20 3
重建后的层序:3 9 20 15 7

重建的关键是“前序给根、中序分段”,左右两段切片必须长度一致、互相对应。

在新标签运行和修改这个实例已装入本课代码 · 可自定义输入 · 可提交判题

04 · 逐步理解

每一步只解决一个问题

  1. 01

    读题:两串序列还原一棵树

    考级常给前序+中序求后序。硬背答案没前途,通用办法是先把树真正重建出来,之后想要什么遍历就遍历什么。先记住两个特点:前序的第一个元素必为根;中序里根左边的全在左子树、右边的全在右子树。

  2. 02

    前序定根

    pre[0] 就是当前这棵(子)树的根。用 ino.index(root_val) 找到它在中序里的位置 k:k 左边有 k 个节点属于左子树,右边剩下的是右子树。根找到了,左右阵营也就分开了。

    PYTHON
    root_val = pre[0]
    k = ino.index(root_val)
  3. 03

    中序分段,切片配对

    左子树在中序里是 ino[:k],共 k 个节点;对应的前序切片是 pre[1:k+1]——跳过根,紧接着的 k 个就是左子树的前序。右子树同理:pre[k+1:] 配 ino[k+1:]。两段长度必须相等,这也是检查题目序列有没有抄错的第一道关。

    PYTHON
    root.left = build(pre[1:k + 1], ino[:k])
    root.right = build(pre[k + 1:], ino[k + 1:])
  4. 04

    递归的出口

    切片每递归一次就小一圈,当某段为空(not pre)时,说明这个方向没有孩子,返回 None。空切片就是这台递归机器的刹车,必须先写。

    PYTHON
    if not pre:
        return None
  5. 05

    重建完再遍历

    树已经在内存里站好了,后序遍历、层序遍历全是老套路:一个递归、一个队列。跑一遍和脑补的树形对照:层序是 3 9 20 15 7,说明 20 是根的右孩子、15 和 7 是它的两个孩子——全对。

  6. 06

    为什么前序+后序不够

    只有前序和后序时,根能确定,但分不清一棵“独生子”子树到底是左孩子还是右孩子,可能对应多棵不同的树。所以考级的标准搭配永远是“前序+中序”或“中序+后序”——中序负责分左右,缺它不行。

05 · 练习与检验

自己写出来,才算真正学会

本课实作步骤
  1. 1运行程序,核对重建出的后序与层序
  2. 2在中序里手指数一数根把序列切成了哪两段
  3. 3换一组前序、中序序列重建自己的树
  4. 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 和数据库各有专门格式与错误处理方式,不能按普通文本随意拆分。
打开本章完整示例与独立阅读页 →

完成检查

确认自己会解释、会编写、会验证