前一篇介绍了树,却未介绍树有什么用。但就算我不说,你也能想得到,看我们Windows的目录结构,其实就是树形的,一个典型的分类应用。当然除了分类,树还有别的作用,我们可以利用树建立一个非常便于查找取值又非常便于插入删除的数据结构,这就是马上要提到的二叉查找树(Binary Search Tree),这种二叉树有个特点:对任意节点而言,左子(当然了,存在的话)的值总是小于本身,而右子(存在的话)的值总是大于本身。
这种特性使得我们要查找其中的某个值都很容易,从根开始,小的往左找,大的往右找,不大不小的就是这个节点了;插入一样的道理,从根开始,小的往左,大的往右,直到叶子,就插入,算法比较简单,不一一列了,它们的时间复杂度期望为Ο(logn)。(为什么是“期望”,后面会讲)
删除则稍微麻烦点,因为我们删的不一定是叶子,如果只是叶子,那就好办,如果不是呢?我们最通常的做法就是把这个节点往下挪,直到它变为叶子为止,看图。
也许你要问,如果和左子树最大节点交换后,要删除的节点依然不是叶子,那怎么办呢?那继续呗,看图:
那左子树不存在的情况下呢?你可以查找右子树的最小节点,和上面是类似的,图我就不画了。
十一、平衡二叉查找树(AVL)
前面说了,二叉查找树方便查找取值插入删除,其复杂度不过为Ο(logn),但这是个“期望值”,因为我们也有比较差的情况,比如下面这棵树:
说是树,其实已经退化为链表了,但从概念上来说它依然是一棵二叉查找树,这棵树怎么形成的呢?很简单,我们只要按着1,2,3,4,5,6,7这样的顺序往一个空的二叉查找树里添加元素,就形成了。这样我们再添加8,9,10……那真的就变成了一个链表结构,那插入的复杂度也就变成了Ο(n)。导致这种糟糕的原因是这棵树非常不平衡,右树的重量远大于左树,所以我们提出了一种叫“平衡二叉查找树”的结构,平衡二叉查找树英文叫AVL,而不是我本来以为的什么Balance BST,AVL来自于人名,我这里就不追究了。
平衡,顾名思义,就是两边看起来比较对称,但很多时候我们是做不到绝对的对称(绝对对称即对任意子树而言,左右节点的数量都相等),因为只有(2^n-1)元素数目的二叉树才能做到绝对对称,所以我们使用了“高度”(height)这么个概念,某节点的高度指的是它离它的子树的叶子的最远距离:
那么我再引申出两个概念,左高和右高:
左高 = 左节点空 ? 0 : (左节点高+1)
右高 = 右节点空 ? 0 : (右节点高+1)
那我们就可以给AVL下个定义了,对AVL的任意节点而言:
ABS(左高 - 右高) <= 1
做到了这点,这棵树看起来就比较平衡了,如何生成一棵AVL树呢?算法十分不简单,那我们先通过图来获得一些最直观的认识,就先按1,2,3,4……这样的自然数顺序加入到树中,下图体现出了树的构造变化:
随着新节点的加入,树自动调整自身结构,达到新的平衡状态,这就是我们想要的AVL树。我们先要分析,为什么树会失衡?是由于插入了一个元素,对吧,那我们能不能把不同的插入情况全部概括起来并作出统一的调整来使得树重新平衡?答案是肯定的,也有人帮我们研究好了,只是证明这个过程需要一些数学功底,我是不行的了,所以直接给出算法示意图和范例。
LL型调整:
再给一个LL型调整的实例:
RR型调整,其实就是LL型调整的镜像而已:
这是一个RR型调整的实例:
接下去就是LR型调整:
这是一个LR型调整的实例:
RL型调整是LR型调整的镜像,所以不再画图了。
至于如何选择不同的调整类型,我后面将给出代码,看“DoBalance”这个函数的实现,很清晰的。那接下去我们还要面临一个比较困难的问题,就是删除及删除平衡,因为不光是插入元素可能导致不平衡,删除也会。不过我们都有个同样的前提,就是无论是插入前还是删除前的二叉树,都是平衡的。
我参考的书上说删除和插入其实是很类似的,具体实现却没说,我后来写代码蛮辛苦的,最后发现确实差别不大,但在调整相关节点高度的时候确实有点细微上的差别,这个在我的代码里也能看得出来。下面我就给出我的代码,我已经通过了初步的测试,不过也许代码还有bug,如果发现了,请留言
原文:http://www.cnblogs.com/yc_sunniwell/archive/2010/06/27/1766236.html