- 极端退化
前面所提到的二叉搜索树,已经为我们对数据集进行高效的静态和动态操作打开了一扇新的大门。正如我们所看到的,BST从策略上可以看作是将之前的向量(动态数组)和链表结构的优势结合起来,不过多少令我们有些失望的是:目前所实现的BST还有些稚嫩,表现在它的时间复杂度在极端情况仍未得到有效的控制。根据之前的内容,我们知道无论静态or动态操作,它的时间上界都正比于树的高度,即O(h),不过到目前为止,我们对于它的高度还没有任何有效的控制方法。比如说,我们假设所有子节点个数都<=1,那么整棵树将会退化为一条单链,此时从逻辑结构上看,它就等价为一个链表。
那么树的高度h与节点数n则呈线性关系,也就是O(n)=n-1=h。这无论做渐进分析还是最坏分析,静态操作和动态操作都需要高达O(n)的时间。
- 平均高度
从客观角度,我们还需要对BST的性能做系统分析。下面从两种统计口径分别给出针对BST平均高度的估算结果。
1.随机生成
对于任意生成的一组关键码词条,长度为n,考虑所有可能的排列,比如n=3的情况。会得到这些形态的树:
这样创建的方式被称为随机生成,当词条长为n时,一共就是n!种可能,平均高度为O(logN)。
2.随机组成
也就是我们将所有n个关键码视作n个互异的积木,在满足BST规则的前提下,考察他们总共能够拼出多少种拓扑结构互异的BST。可以证明这样得出的总数为Catalan(n),是我们熟知的卡特兰数,组合数学里讲过。这时候平均高度为O(√N)。
那这两种口径得到的结果不一样,到底哪种更为可信呢?对比一下
我们认为,后者更可信,原因在于前者是有所重复的,这种重复性就体现在不同的关键码序列有可能会生成同一棵BST,比如对于n为3的情况而言,这样两个输入序列所生成的都是这样同一棵BST。
也就是说只要第一个插入的是居中的2,那么分列于它左右的1和3究竟是按什么次序输入是没有关系的。实际上不难理解,这个结论可以进一步地推广,也就是说中位数,或者接近于中位数的关键码越是被更早地插入,整体而言,这棵BST的高度也相应地会更低。这就意味着在前一种统计口径中。
这类高度更低的BST,将会被以更高的重复度参与统计以及最终的平均估算。这也是为什么按照前一统计口径所得到的估算值会相对更小。从这一观点来看,前一统计口径可以说是过于乐观了,而后一统计口径所得到的结论将更为可信。但对于在此非常在意树高的我们来说,这并不是一个好消息,这意味着在天然的随机意义下,这样一个高度是超出我们承受范围的,为了进一步地降低和控制这个高度,我们应该采取措施。
- 理想+适度
为了控制树的高度,我们或许首先应该弄明白:什么样的树相对而言高度是更低的。根据之前的例子,在节点数目相对固定时,左右兄弟子树的高度越是接近,全树通常也会更加倾向于高度更低。也就是说——全树越是接近于平衡,那么它的高度也会倾向于更低。因此我们可以通过控制全树的平衡度,以控制全树的高度。关于树的高度 我们有这样一个结论,由n个节点所组成的二叉树,其高度最低不会少于log 2为底n的对数。
因此如果某棵树的高度能够达到这样一个理想的下限,我们也称之为理想平衡的。那么哪些树能够达到这样的理想平衡状态呢?你应该会联想起我们此前所讲过的——完全二叉树( Complete Binary Tree)。
是的,在这样的树中,叶节点只能出现在最底层以及次底层。当然 其中最好最好不过的自然是所谓的满二叉树( Full Binary Tree)。
然而很遗憾——这样的树在实际应用中只能是可遇不可求的。
而且即便BST在某一个时刻,能够达到这样高度紧凑的形式,在接下来的动态操作过程中这样一种完美的形式也是难以持续的。因此所谓的理想平衡,在实际应用中是不具任何意义的,理想平衡出现的可能性,非常非常的低。而且为了维护这样的理想平衡,我们的计算成本也相应地会十分的高昂,会得不偿失。
而真正可行的方法是,我们或许应该适度地放松平衡的标准,没错 适度地放松。你或许会联想起我们此前的渐近分析,没错,那正是我们的一个法宝。实际上只要能够保证全树的高度,能够从渐近的意义而言不超过logN,那么也就可以称之为是平衡的了。
因为这种平衡并非严格意义上的理想平衡,所以我们也不妨称之为适度平衡。这样一种适当的放松,但又不失原则的方法,也就是我们通常所谓的退一步海阔天空。那么相应地——能够保持适度平衡的BST,也称作平衡的二叉搜索树(Balanced Binary Search Tree),简称BBST。也就是说,如果将所有的BST视作一个全集,那么BBST只是其中的一个子集。对于目前而言其中任何一棵BBST,如果经过某次操作之后它不再保持适度平衡。也就是——会游离到这个子集之外,果真如此的话我们就需要有一整套方法将这棵BST重新拉回到这个子集中,使它重新成为一棵BBST。像这样:
那么我们应该采用什么样的一些方法来做到这一点呢?或者先退一步,我们来考虑一下:我们可能采用哪些方法呢?
概括而言,所有这些方法都必须是所谓的等价变换。
- 歧义=等价
所谓BST,它的本质特征,就是处处局部的顺序性、以及全局的单调性。具体来说——只需要考察它的中序遍历序列是否是单调的。然而只需考察树的遍历算法,我们就不难发现:结构不相同的两棵BST的中序遍历序列有可能是完全雷同的,这也就是我们所说的中序遍历序列的歧义性。在某些场合中,比如中缀表达式的求值计算。这种中序遍历序列的歧义性非常令人生厌,因为我们不得不通过一些办法,来明确地辨析不同操作符之间的优先级关系。而针对BBST这样的一个问题,歧义性却变成了一个非常重要同时也是不可或缺的一种性质。
以这里所给出的两棵BST为例,不难看出它们都是由同一组关键码所构成的,而且它们的中序遍历序列是完全一样的,然而反过来 我们也注意到,它们的拓扑结构也不尽相同。左侧局部子树的树根是19,而在右侧却是16。当然,还可以很容易地举出更多拓扑结构不尽相同,但中序遍历序列却相同的实例。任何一对这样的BST也就称作相互等价的BST。等价的BST之间在拓扑结构上虽然不尽相同,但也有其独特的规律。
这种规律概括起来有两句话:
第一:“上下可变”比如在这个例子中19和16的祖先和后代关系就有可能在两棵树中彼此颠倒。这也可以认为等价的BST在垂直方向有一定的*度。然而我们的第二条规律则是“左右不能乱”,这里的左右自然是指中序遍历序列。确实相对于任何一个节点,居于它右侧的节点以及居于它左侧的节点之间不能相互混淆。那么在这样一种上下存在一定的调整余地,但左右次序却不得颠倒的规则下又该如何来实现BST之间的等价转换呢?
- 等价变换
实际上对于任何一棵BST之间的等价转换,都可以视作是由一系列的基本操作串接而成的,而这种基本的变换无非两类 彼此对称,其中一类如这个图所示:
也就是说 如果节点V拥有一个左孩子C,而且它们属下分别有三棵,在此命名为X Y Z的子树,我们只需将这局部的两个节点以及三棵子树重新调整为这样一种拓扑连接的形式。那么无论是在此局部,还是在它们所属的那棵全树,顺序性和单调性将依然保持。也就是说 全树依然将是一棵BST。为了对此做一验证,我们不妨来考察在此局部的中序遍历次序——我们可以看到:无论是在变换之前 还是变换之后,在此局部的中序遍历序列的次序必然是X C Y V 以及最后的Z。从效果来看 这样一个变换,可以大致理解为是在此局部围绕着节点V,做了一个顺时针的旋转。称这种变换为zig。那么这种旋转的中心V——则是zig的参数。
下面则是完全对称的另一种基本操作:
因为它可以理解为是围绕着节点V做了一次逆时针的旋转操作,我们也形象地称之为zag。在后续的章节中 我们将会看到包括AVL树、 红黑树在内的各种BBST都分别精心地定义了某种适度平衡的准则,从而使得原本在其中的任何一棵BBST即便在经过某次操作之后,会暂时地游离到这个边界之外,我们也总是能够通过一系列精巧地等价变换令它重新回到这个边界以内,并重新成为一棵BBST。
而在设计所有这些等价变换的组合时,我们始终不要忘了应该遵循两个重要的准则:一个就是所谓的局部性。也就是说,我们执行的每一次等价变换都应该局限在某一常数规模的局部。比如对于我们刚刚介绍的zig和zag操作而言,它们都局限在局部的V和C两个节点处。
如此一来它们所牵涉到的节点总数既然是常数,这类操作所需要的计算时间也可以严格地控制在常数的规模。
第二个需要严格遵守的是:在我们将一棵刚刚失衡的BBST重新恢复为一棵BST的过程中,累计需要执行的旋转次数不要过多。比如至多不能超过logN次,这样我们就可以有效地控制整个操作序列的长度、以及总体所需要的时间。后面我们会看到 任何一种BBST都必须至少满足这样一个logN的基本条件。但是在进一步的要求上,它们各自又有所差异——比如对于AVL树而言,它的删除操作只能刚刚达到这个及格线,而它的插入操作却可以优化到常数。而我们在后面的后面将要介绍的红黑树,也就是Red Black Tree则可以进一步地将这两种操作的性能同时提升到最优的常数。那么接下来的一节,我们就首先来看看,AVL树是如何达到这样一个及格线的。