【第53期】有序插入把二叉搜索树变成链:别只看“树”的名字

文章来源声明: 原文作者:伞伞悦读; 来源站点:掘金; 原文链接:https://juejin.cn/post/7691902169553616932; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

用最小实验戳破“树就一定快”的幻觉,适合被有序时间戳、递增ID坑过的开发者。先量树高再选平衡树或数据库索引,能避免线上热路径的O(n)尾部延迟。

> 系列:《从小白到 AI 大模型开发工程师的进阶之路》 技术点:AI-0207 树与二叉树 主人公:小蓝伞|环境:Windows 11、Python 3.13.9

小蓝伞用二叉搜索树保存递增时间戳,以为查找天然是对数复杂度;63 个键插入后树高就是 63,已经退化成链。随机插入同样数量的树高约 15,完全平衡约 6 层。本文先用最小实现证明“高度才决定查找成本”,再说明什么时候必须换平衡树或数据库索引。

一、现场与结论

二叉搜索树只保证左小右大,不保证两边平衡。推荐先记录高度、用中序遍历校验有序性,再决定是否使用平衡树。不要把有序输入的成功查询当成树性能结论,也不要在生产热路径手写未平衡 BST。

二、方案取舍

方案优点代价适用条件
普通 BST实现直观最坏 O(n)教学、输入随机且可控
AVL/红黑树高度受控旋转和实现复杂动态有序集合
数据库索引持久化、并发成熟有 IO 和建索引成本业务数据检索

三、最小复现

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 和时间戳。