声明:AVL树是以搜索二叉树为基础的树,读者看之前需要掌握相关的知识(搜索二叉树)、
搜索二叉树不足
搜索二叉树的因为树的结构不稳定,导致它的速度不能稳定维持在O(logN)级别,大多情况是O(N)级别。解决的办法也很简单,就是在插入的时候通过一些操作保证树的高度差尽量保证0到1之间。这样就有了稳定的O(logN)级别的速度。
对此,出现了两种常见的处理方案来完成这种操作。一是AVL树,通过平衡因子来控制检测树的左右高度差,并通过旋转来保持高度差在1以内;而是红黑树,通过红黑染色和旋转来保持高度差。
本博客仅先讲解AVL树,红黑树的讲解且看红黑树博客。
AVL树的插入
AVL树本质上还是搜索二叉树,只是在插入的基础上对节点做出一些操作保证平衡性。
平衡因子
那么AVL树是怎么检测平不平衡呢?就要用到平衡因子,每个节点都有一个平衡因子,它是当前节点的右子树的高度减去左子树的高度差值。
这里用bf(balance factor)表示平衡因子。
那么bf范围应该是多少呢?是否可以是任何值呢?
答案肯定不可以是任何值。因为AVL树的高度差是小于1的,那么bf的范围只能在[-1,1]的范围波动
bf=0 左右子树高度相等 bf=-1左子树比右子树高度大一 bf=1 右子树比左子树大一
当然有特殊情况,就是插入的时候,我们某个节点的一边会多增加一个节点,导致层数的增加,如果刚好加到高的那一边,那么就会出现bf=2或者bf=-2的情况,此时就要用到我们上面提到的旋转。
那么这样一分析,我们插入的过程就知道了,现是正常让一个值插入到节点里面,然后就要从这个节点往上更新。
插入时平衡因子的更新
为何从这个节点往上更新呢?因为插入的值始终是作为叶子节点插入的。此时这个插入的节点就可以把它的bf设为0,然后往上面更新。
既然往上更新,那么就不能用简单的搜索二叉树那样只用左右节点,还要有父节点指针。
那么我们的AVL树的节点类就可以定义出来了:
我们继续说平衡因子的更新
我们往上更新,如果上来的是父节点的左节点,那么bf就要-1,反之+1,这个不难理解
平衡因子往上更新停止的条件
那么我们一直往上更新,什么时候停止呢?是否每次都是更新到了root节点才停止呢?
答案是否定的。
- 当一个节点更新后bf=0,说明之前是左右有高度差,而现在平了,那么这个节点往下的最大高度是没有变化的,因此bf=0时就可以停止往上更新平衡因子了。
- 当一个节点更新后|bf|=1,说明之前左右是平的,插入了一个值导致不平衡了,那么这个节点往下的最大高度就增大一个高度了,因此还要继续往上更新。
- 当一个节点更新后|bf|=2,说明之前是左右不平的,现在又往高的一方插入一个导致更高了,这时我们需要进行旋转。
- 当节点到达root节点时,也不需要往上更新了,已经到顶了。
如下:值为1的节点更新是情况2,值为3节点更新是情况3
如图:值为14节点更新是情况2,值为10的节点更新是1情况3,此时已经不是AVL树了,需要旋转
如下图:值为8更新对应的是情况4
那么我们就可以写出插入和更新的代码:(这里我用K结构和去重结构)
旋转
我们所有的情况都处理完了,现在就差旋转没有解决了,我们来看看什么是旋转:
旋转的原则
1. 保持搜索树的规则
2. 让旋转的树从不满足变平衡,其次降低旋转树的高度
旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋。
右单旋
右单旋就是如上图所述,整个是向右边旋转的。原理就是10一定比左子树都大,那么10可以连接5的右子树到左子树来,然后5作为根将10作为它的右子树。这样就让bf从2变为0。
我们再看几个具体的例子:
这里我们注意到右单旋的满足条件是根节点bf=2,根节点左节点的bf=1。
我们进行代码实现:
左单旋
就是右单旋的对称,这里就不过多讲解了
代码实现:
左右双旋
双旋的情况更复杂:
我们先找规律,这里的节点插入不是像左单旋和右单旋医院插入到两边,而是插入在片内部的区域,这里就是插入在subL的左右节点区域。旋转后,subLR会变为根节点,subLR的左右节点会分开到左右两边去。
根据subLR的bf值,可以分为三种情况:
- bf=0时,只有一种树的情况,就是如图,这种情况我们需要将最后三个节点的bf都置为0
- bf=1时,说明增加的值插入在右边,最后bf从左到右要更新为-1 0 0
- bf=-1时,说明增加的值插入在左边,最后bf从左到右要更新为 0 0 1
bf=1或-1更新的-1和1是因为subLR中最短的那个树造成的,它是四个子树中最短的一条,分到哪边,就是哪边最短的。因此会出现1或者-1的bf更新值。
代码实现:
因为有左单旋和右单旋的代码实现,我们这里主要是对三个节点bf的更新:
看到注释了吗,这玩意坑死我了,在经过左单旋和右单旋后,subLR的bf已经不是原来的bf了,会变成0,导致出现极其隐蔽的错误!
右左双旋
和左右双旋是对称的:
依然是分三点,看subRL的bf
- bf=0 三个都是0
- bf=1 最左边的为-1
- bf=-1 最右边的为1
原理一样的,直接看代码:
那么最后我们的插入函数就完善了:
AVL树检测:
首先就是用查找函数进行检测:
但是不是十分的准确,应该从高度差和平衡因子入手:
效率分析
这就不必多说了,肯定是杠杠的logn。
AVL树的删除(了解即可)
要知道AVL树的删除,我们首先要知道普通搜索二叉树的删除是怎么搞的。不会的就去看我的搜索二叉树的博客。
那么我这里接给出搜索二叉树的删除节点的总结:同时我讲这几种情况平衡因子的更新是如何的,我用红色标记为平衡因子的更新解释
- 如果删除的是叶子节点,那么就直接删除这个叶子节点即可。此时的高度会降,那么就要向上更新平衡因子,必要的时候进行旋转。
- 如果删除的节点一边有节点,另一边是空,设这个节点叫del。那么可以直接删除del,让它的存在的子树直接和del的父亲相连。同时考虑root的特殊情况,需要更新root。此时del的子树高度差不会变化,变化从del的父亲开始,那么就从这开始向上更新平衡因子,必要的时候进行旋转
- 如果删除的节点左右子树都有节点,依然设这个节点为del。我们可以找del左子树最大的节点或者右子树最小的节点进行值交换,然后把左子树最大节点或者右子树最小节点删掉。这里就用右子树最大节点来操作,设这个节点为swap。那么我们就要找del右子树的swap节点,有可能就是右子树的根,即这颗右子树没有左子树,那么我们直接把swap的值赋给del,然后删除swap节点,让swap的右子树连接到del上,此时swap的高度差不变,变化的是del的平衡因子,因此从del开始向上跟小平衡因子,必要的时候开始旋转。如果del的右子树有左子树,那么就一直往它的左边走,找到最左边的节点作为swap节点,这个节点就是del右子树的最小值,此时我们直接把swap的值赋给del,然后删除swap节点,同时让swap的右子树连接到swap的父亲节点的左边,此时swap的右子树高度差依然不变,变的是swap父亲的平衡因子,因此从swap的父亲开始向上更新平衡因子,必要的时候进行旋转。
这里我讲的较为抽象,不知道的可以看我之前的博客
那么我们先放出没有平衡因子更新的删除:
这里我们可以看到,向上更新的过程经常发生,那么直接就将这个过程搞成一个函数,方便我们写代码。否则有很多的冗杂代码。毕竟二叉搜索树的删除代码也是比较复杂的。
那么就可以合在一起了:
这里我就不细讲了,具体细节大家自己考究,因为这个在面试里面都不做要求,一般只会考一下插入,旋转。
作者吐槽:AVL树细节多的要死,需要大家在脑子清醒的时间段写,不然bug会很多,毕竟不能像其它数据结构一样可以写一点测一点,AVL树必须把四个旋转都写了,完成插入操作后才能检测得到是否有问题。因此务必要理解旋转的代码,并且注意细节问题。例如能多存那几个节点的指针就多存,不要贪内存,防止混乱或者指针地址变化导致最后的错误。又例如双旋要提前存bf不然坑的死死的。总之务必小心,不然极其容易让自己崩溃。