系列:《从小白到 AI 大模型开发工程师的进阶之路》 技术点:AI-0207 树与二叉树 主人公:小蓝伞|环境:Windows 11、Python 3.13.9
小蓝伞用二叉搜索树保存递增时间戳,以为查找天然是对数复杂度;63 个键插入后树高就是 63,已经退化成链。随机插入同样数量的树高约 15,完全平衡约 6 层。本文先用最小实现证明"高度才决定查找成本",再说明什么时候必须换平衡树或数据库索引。
一、现场与结论
二叉搜索树只保证左小右大,不保证两边平衡。推荐先记录高度、用中序遍历校验有序性,再决定是否使用平衡树。不要把有序输入的成功查询当成树性能结论,也不要在生产热路径手写未平衡 BST。
二、方案取舍
| 方案 | 优点 | 代价 | 适用条件 |
|---|---|---|---|
| 普通 BST | 实现直观 | 最坏 O(n) | 教学、输入随机且可控 |
| AVL/红黑树 | 高度受控 | 旋转和实现复杂 | 动态有序集合 |
| 数据库索引 | 持久化、并发成熟 | 有 IO 和建索引成本 | 业务数据检索 |
三、最小复现
python
class Node:
def __init__(self, key):
self.key, self.left, self.right = key, None, None
def insert(node, key):
if node is None:
return Node(key)
if key < node.key:
node.left = insert(node.left, key)
else:
node.right = insert(node.right, key)
return node
def height(node):
return 0 if node is None else 1 + max(height(node.left), height(node.right))
分别插入 range(63) 和随机打乱后的同一组键,打印 height(root)。正常判据是中序遍历有序;失败输入是严格递增序列,预期高度接近节点数。
四、误判、实验与原理
误判是"树一定比列表快";定位证据是高度曲线而非单次查询;根因是每次插入都沿同一侧向下;修复是随机化、旋转或换成熟索引。实验固定键集合,分别测 63、1,000、10,000 个键,重复 5 次取中位数,并同时记录高度。递增输入的高度随 n 增长,随机输入才接近对数;这组实验不能证明所有平衡树实现的绝对耗时。
树的对数查找依赖每层都能排除一部分节点。层序遍历应使用 deque,不能用列表 pop(0),否则会复现第 51 期的搬迁税。深度优先递归还受 Python 递归深度限制,大树应改显式栈。
五、验证清单与边界
- 空树、重复键、单节点都要有明确策略。
- 中序遍历必须递增;递增输入高度不得被误标为"平衡"。
- 生产环境优先数据库 B-Tree、标准库或成熟平衡树库。
- 本文实现用于理解退化,不承担并发、持久化和删除后的平衡维护。
总结与下一期
树的形状比抽象名更重要。下一期 AI-0208 堆与优先队列会把"只要 Top-K,不必全量排序"的取舍落到数据上。
官方资料
工程追问
如果输入已经按时间递增,随机打乱后再插入能否解决问题?它能降低普通 BST 的退化概率,但不能给出最坏情况保证;重启或不同随机种子还可能让延迟尾部变化。需要稳定延迟时,应直接选 AVL、红黑树或数据库索引。删除节点后也要重新测高度,不能只验证插入阶段。
如何证明树是否适合当前业务?先列出查找、插入、删除的比例,再记录节点数、树高和 P99 延迟。若查找远多于更新,可以构建静态有序数组并用二分;若更新频繁且要求范围查询,成熟索引通常比手写节点更可维护。树的遍历顺序也属于接口:中序输出有序,前序适合序列化,层序适合按层处理,不能只测一种遍历。
本期结论可以迁移到索引、路由表和时间窗口:任何声称对数复杂度的结构,都要把"高度或层数如何被维持"写进设计。没有平衡机制时,最坏输入不是理论上的敌人,而是排序后的日志、递增 ID 和时间戳。