在软件设计师的上午综合知识里,数据结构与算法是雷打不动的重点板块,而树形结构又是其中命题密度最高的一族。多数考生对二叉树的三种遍历背得滚瓜烂熟,前序、中序、后序的序列张口就能报,可真把题目切换到二叉排序树这个具体场景,很多人就开始发懵:为什么同样是二叉树,二叉排序树的中序遍历恰好是有序的?为什么插入一个结点永远只能落在叶子位置?为什么删除结点要分三种情形讨论、每一种的替代规则又不一样?这些看似零散的问题背后,其实藏着一套自洽且严密的设计逻辑。本文从概念定义出发,一路拆到查找、插入、删除的底层机制,再结合历年真题的命题套路,把二叉排序树这个考点一次讲透。
二叉排序树,也称二叉查找树、二叉搜索树,英文缩写为BST。它的名字里同时出现了"排序"和"查找"两个功能指向,这本身就暗示了它的双重身份:既是一种能自然维持元素有序关系的存储结构,又是一种高效的动态查找结构。理解这个双重身份,是吃透后续所有机制的起点。
教材对二叉排序树给出的定义,既包括空树的情形,也包括非空树的情形。一棵二叉排序树要么是空树,要么满足以下三条约束:第一,若左子树不空,则左子树上所有结点的值均小于根结点的值;第二,若右子树不空,则右子树上所有结点的值均大于根结点的值;第三,左子树和右子树本身也各自是一棵二叉排序树。这里需要特别留意的措辞是"所有结点",而不是"左孩子"。很多初学者把定义简化成"左孩子小、右孩子大",这在局部上没错,却漏掉了递归的约束范围。真正的定义要求的是整棵左子树的所有结点都比根小、整棵右子树的所有结点都比根大,这一层递归约束正是保证中序遍历全局有序的根本原因,缺了它,二叉排序树的有序性就会被破坏。
形式化定义里还有两个容易忽略的细节。其一是"严格小于"和"严格大于"的表述。标准定义默认结点的值互不相同,因此用的是严格不等号;一旦涉及允许重复值的情形,通常需要约定相等的值统一放在某一侧,否则查找和删除的语义会产生歧义。其二是递归的自指性。定义说左子树和右子树本身也是二叉排序树,这意味着二叉排序树的有序性是逐层向下传递的,任意一个结点拎出来,以它为根的整棵子树都满足同样的性质。这个递归特性是后面推导查找路径、插入位置、删除替代方案的理论基石。
普通二叉树对结点值的排列没有任何约束,结点可以按照任意顺序摆放在树中;而二叉排序树给结点值的分布强加了一套严格的大小关系约束。正是这套约束,让二叉排序树从"一棵普通的树"升级成了"一个可以支持高效查找的有序容器"。可以这样理解:普通二叉树定义的是"形状",二叉排序树定义的是"形状加秩序"。形状决定了存储,秩序决定了查找效率。
这个区别在算法层面带来一个直接后果:对普通二叉树进行查找,最坏情况下需要遍历整棵树,时间复杂度是线性的;而对二叉排序树进行查找,每一步都可以通过与根结点比较大小,把搜索范围果断地砍掉一半,形成一个从根出发、沿着某一条路径逐层下探的过程,平均查找效率大幅提升。换句话说,二叉排序树的价值不在于它是一棵二叉树,而在于它的结点值分布被秩序化了,从而让"折半"这个思想得以在链式存储的树结构上落地。
二叉排序树最经典的一条性质,就是中序遍历所得到的结点值序列,必然是一个递增的有序序列。这条性质的推导非常直观:中序遍历的顺序是"左子树、根、右子树",根据定义,左子树所有结点小于根、根小于右子树所有结点,那么按这个顺序输出,就会先输出所有较小的值,再输出根,最后输出所有较大的值;而左子树和右子树内部又各自满足同样的递归性质,于是整条序列从上到下、从局部到全局逐层有序。
这条性质是命题人最爱下手的地方,也是解题的一把万能钥匙。反过来,这条性质提供了两个推论。推论一:给定一个中序序列,配合任意一个先序或后序序列,可以唯一确定一棵二叉排序树,这与普通二叉树"先序加中序才能唯一确定"的规律保持一致。推论二:因为中序有序,二叉排序树天然支持快速查找第k小的元素、快速查找某个值的前驱和后继等操作,这些能力在普通二叉树上都无从谈起。理解了"中序有序"这条核心性质,后续的查找、插入、删除机制就都有了一个统一的解释框架。
二叉排序树的一切操作,本质上都是围绕"与根比较、向一侧递归"这一条主线展开的。查找是这条主线的原始形态,插入是查找的延伸,删除则是三种操作里最复杂的一种,因为它要在移除结点之后重新缝合树的秩序。逐一拆解这三种操作,才能真正理解二叉排序树作为动态查找结构的精妙之处。
查找操作的流程极其简单,却蕴含着二叉排序树效率的全部秘密。给定一个目标值,从根结点开始比较:若目标值等于当前结点的值,查找成功;若目标值小于当前结点的值,则转到左子树继续查找;若目标值大于当前结点的值,则转到右子树继续查找;如果一路下探到空指针仍未找到,则查找失败。这个流程的每一次比较,都能确定性地排除一棵子树,让搜索范围以近似折半的速度收缩。
查找效率可以用查找长度来度量。查找一个结点所需比较的次数,等于该结点在树中的深度加一。整棵树查找性能的好坏,由平均查找长度ASL来衡量,它等于所有结点的查找长度之和除以结点总数。当二叉排序树接近平衡、形态近似完全二叉树时,树的高度约为以二为底结点数的对数,此时平均查找长度也处于对数级别,这是二叉排序树能达到的最佳状态。然而当树严重倾斜、退化成一条单链时,查找就退化成顺序查找,平均查找长度回到线性级别。可见二叉排序树的查找性能并非天生优秀,而是高度依赖于树的形态,这一点正是后续引入平衡二叉树的思想源头。
插入操作是查找操作的直接延伸。要插入一个新值,先按查找的路径从根出发,一路比较下探,直到抵达一个空指针位置,把这个新结点挂在那里即可。由于这个位置正是查找该值时"找不到"的落点,因此插入的结点必然成为叶子结点。这个结论非常关键,它意味着插入操作永远不会改变已有结点之间的父子关系,只是给树添加了一个新的叶子。
"插入必为叶子"这一特性带来两个可以引申的考点。其一是插入顺序决定树的形态。同样的元素集合,按不同顺序插入,会得到形态完全不同的二叉排序树。比如按升序依次插入,每次新值都比当前所有结点大,于是每次都挂在右子树的末端,最终退化成一条右斜的单链;而按一个均衡的次序插入,则可能得到一棵相对平衡的树。其二是插入的平均代价与树的当前高度正相关。树越高,插入需要比较的次数越多,这也再次印证了形态对性能的决定性影响。
删除是二叉排序树三种基本操作中最棘手的一个,因为它要在摘除结点之后,重新维护整棵树的大小秩序。根据被删结点的子树情况,删除分为三种情形,每一种的本质都是在"删掉该结点"与"保持中序有序"之间做一次精密的缝合。
情形一,被删结点是叶子结点。这种情况最简单,直接摘除即可,不会牵动任何其他结点,也不会破坏有序性。情形二,被删结点只有一棵子树,左子树或右子树其一为空。此时只需让被删结点的唯一子树直接顶替它原来的位置,因为子树中所有结点的值本就整体落在被删结点值的同一侧,顶替之后与祖父结点的相对大小关系依然成立,秩序得以保持。情形三,被删结点左右子树均不为空。这是最复杂的情形,不能简单地让某一棵子树顶替,因为两棵子树的值分居两侧,任何一侧单独顶替都会破坏与另一侧的关系。标准做法是找到被删结点的中序前驱或中序后继,用它来替换被删结点,然后再删除那个前驱或后继结点。所谓中序前驱,是左子树中值最大的结点,即沿左子树一路向右走到尽头;所谓中序后继,是右子树中值最小的结点,即沿右子树一路向左走到尽头。用前驱或后继替换后,替换者的值恰好处于左子树与右子树的值域之间,完美承接了原结点的秩序定位,而替换者本身至多只有一棵子树,它的删除又回到情形一或情形二,问题被规约简化。
删除操作的这三大情形,是软考选择题里最常出现的陷阱来源。命题人尤其喜欢考察双子树删除时前驱和后继的选取规则,以及替换之后递归删除的落点。能否把三种情形从"死记"上升到"理解缝合逻辑",决定了考生在删除类题目上能否稳定拿分。
二叉排序树并不是一个单一形态的静态结构,它的形态在"平衡"与"退化
本篇完!