我们知道,二叉搜索树是会出现单向的。单向在查找时效率是非常低的,时间复杂度会退化成O(N),而AVL树就是解决这个问题。
文章目录
1. AVL 树
1.1 AVL树的概念
概念:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右
子树高度之差的绝对值不超过1(需要对树中的结点进行调整)。
性质:1. 它的左右子树都是AVL树。2. 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。
1.2 AVL树节点的定义
AVL树并没有规定必须要设计平衡因子,只是一个实现的选择,方便控制平衡。
1.3 插入后的平衡因子
首先,我们还是按照之前插入的方式:
这里主要是插入的父结点要更新。
那么一个新结点插入树中后,它的平衡因子怎么变化(是右子树-左子树为例),我们来看:
我们在9的左边插入一个新结点,哪些平衡因子会发生变化呢?
我们可以看到,需要改变的是插入结点的祖先。其余结点不需要改变。
每个祖先的平衡因子如果改变成1或者-1,说明原来是0,子树的高度发生了变化,需要继续往上更新。
此时,父亲的平衡结点为0。说明parent为根的树高度不变,不需要在往上更新了。
这里parent的平衡因子变成了2,违反了AVL树的规则,就不需要再往上更新了,我们要旋转子树。
基本规则如下:
1.子树高度变了,就继续往上更新。
2.子树高度不变,就完成更新。
3.子树违反平衡规则,就旋转子树。
代码实现:
1.4 AVL树的旋转
根据节点插入位置的不同,AVL树的旋转分为四种:
1.4.1 右右:左单旋
1. 新节点插入较高右子树的右侧—右右:左单旋
左单旋是将60的左边放到30的右边,然后将30放到60的左边。
代码实现思路:
我们先将我们要修改的位置给标记上:
我们用parent,SubR,SubRL来标记我们要修改的结点。我们可以根据上图来分析,parent改到SubR的左边,SubRL改到parent的右边。
还有我们要把三叉链中的父指针更新:
这里我们还要注意:SubRL可能为空,所以我们需要加一个判断。
到这里了,还是没有结束。因为parent可能是根,也可能不是根。
这是parent不为根且SubRL为空的情况,旋转之后为:
我们需要将7和9(SubR)连起来。
左单旋的平衡因子:
从上图我们可以看出,只有parent和SubR的平衡因子发生了变化。SubRL的高度并没有发生改变。
从上图也可以看出,当parent的平衡因子为2,cur的平衡因子为1时。就会发生左单旋。
1.4.2 左左:右单旋
2. 新节点插入较高左子树的左侧—左左:右单旋
这里把30的右边转到60的左边,然后把60转到30的右边。
代码实现思路:
还是要将修改的位置给标记上:
我们将SubLR放到parent的左边,把parent放到SubL的右边。其余和上面的左单旋类似。
1.4.3 左右:先左单旋再右单旋
3. 新节点插入较高左子树的右侧—左右:先左单旋再右单旋
什么是较高左子树的右侧?
当然在b插入或者c插入都会引起这种情况。
那么我们该怎么旋转呢?
将双旋变成单旋后再旋转,即:先对30为根进行左单旋,然后再对90为根进行右单旋。
但是还有一种平衡因子变化,没有考虑到。
因为h为0的时候,平衡因子不一样了。
虽然有三种平衡因子的情况,但是旋转的方法都是一样的。
那么我们该怎么区分这三种情况呢?
答案是:观察60这个结点的平衡因子。
代码实现:
我们还是先给这三个位置标记上。
但是此时我们左旋转和右旋转后,树的平衡因子被打乱了。我们需要重新更新平衡因子。根据上图的三种情况来更新,会比较容易。
1.4.4 右左:先右单旋再左单旋
4. 新节点插入较高右子树的左侧—右左:先右单旋再左单旋。
那么这个情况和上一种类似,我们直接来看图:
将双旋变成单旋后再旋转,即:先对90为根进行右单旋,然后再对30为根进行左单旋。
代码实现:
1.5 AVL树的验证
1.6 AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。