AVL 树是一种常见的平衡二叉搜索树
一、背景
1、问题
例如:在 n 个动态的整数中搜索某个整数是否存在?
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
31 | 66 | 17 | 15 | 28 | 20 | 59 | 88 | 45 | 56 |
如果维护一个有序的动态数组,使用二分搜索,最坏时间复杂度:O(logn)
但是添加,删除的平均时间复杂度是O(n)
针对这个需求,有没有更好的方案?
使用二叉搜索树,添加,删除,搜索的最坏时间复杂度均可优化至:O(logn)
2、二叉搜索树
二叉搜索树是二叉树的一种,是应用非常广泛的一种二叉树,英文简称为 BST,有如下特点:
- 任意一个节点的值都大于其左子树所有节点的值
- 任意一个节点的都小于其右子树所有节点的值
- 它的左右子树也是一颗二叉搜索树
- 二叉搜索树存储的元素必须具备可比性,如果是自定义类型,需要指定比较方式,不允许为 null
根据二叉搜索树的定义,如果是按照 7,4,9,2,5,8,11 顺序添加可以构造一颗完全二叉树。
如果是从从小到大或者从大到小添加节点,二叉搜索树树将会退化成链表。此外,删除节点也有可能会导致二叉搜索树退化成链表。
如何避免二叉搜索树退化成链表呢?
首先,节点的添加,删除顺序是无法限制的,可以认为是随机的。所以,改进方案是:在节点添加,删除操作之后,想办法让二叉树恢复平衡。一颗达到适度平衡的二叉搜索树,可以称之为:平衡二叉搜索树,英文简称 BBST
二、AVL树简介
AVL树是最早发明的自平衡二叉搜索树之一,AVL 取名于两位发明者的名字(G.M.Adelson-Velsky 和 E.M.Landis)。
也有人把 AVL 树念做 “艾薇儿树”。
- 每个节点的平衡因子只可能是 1,0,-1 (绝对值 <=1,如果超过1,称之为“失衡”)
- 每个节点的左右子树高度差不超过1
- 搜索,添加,删除的时间复杂度是O(logn)
如下图所示:添加节点13之后,13的祖父节点14平衡因子为2,已经失衡了
三、添加导致的失衡
二叉搜索树是在叶子节点上添加节点
最坏情怀:可能会导致所有祖父节点都失衡
父节点,非祖先节点都不可能失衡,需要通过以下方式旋转恢复平衡
T0,T1,T2,T3 可能为空,长度和位置关系仅供参考,主要是需要关注旋转
1、LL - 右旋转(单旋)
如下图所示:添加红色节点后,g的平衡因子为2,失衡了
这属于在左子树的左子树上插入节点之后导致的失衡,通过右旋转 P 节点成为根节点,恢复平衡
节点大小关系如下:
n < p < T2 < g < T3
所以 p 节点右旋转成为根节点时,需要做以下调整:
- g 成为 p 的右子树(p.right = g)
- T2 大于 p,小于 g,T2之前是 p.right。所以 T2 成为 g 的左子树 (g.left = p.right)
此时整颗树达到了平衡,此外还需要注意一些收尾工作:例如:更新 T2,p,g 的parent 属性
调整之后如下所示:
部分实现代码如下:
private void rotateRight(Node<E> grand) {
Node<E> parent = grand.left;
Node<E> child = parent.right;
grand.left = child;
parent.right = grand;
afterRotate(grand, parent, child);
}
2、RR - 左旋转(单旋)
如下图所示:添加红色节点后,g的平衡因子为 -2,失衡了
同理:这属于在右子树的右子树上插入节点之后导致的失衡,通过左旋转 P 节点成为根节点,恢复平衡
节点大小关系如下:
T0 < g < T1 < p < n
所以 p 节点左旋转成为根节点时,需要做以下调整:
- g 成为 p 的左子树(p.left = g)
- T1 大于 g,小于 p,T1之前是 p.left。所以 T1 成为 g 的右子树 (g.right = p.left)
此时整颗树达到了平衡,此外还需要注意一些收尾工作:例如:更新 T1,p,g 的parent 属性
调整之后如下所示:
部分实现代码如下:
private void rotateLeft(Node<E> grand) {
Node<E> parent = grand.right;
Node<E> child = parent.left;
grand.right = child;
parent.left = grand;
afterRotate(grand, parent, child);
}
3、LR - RR左旋转,LL右旋转(双旋)
如下图所示:添加红色节点后,g的平衡因子为 2,失衡了。
这属于在左子树的右子树上插入节点之后导致的失衡,此时需要先让 p 节点左旋转,然后让 g 节点右旋转
左旋和右旋和上面的操作一致,不再赘叙
旋转过程如下:
4、RL - LL右旋转, RR左旋转(双旋)
如下图所示:添加红色节点后,g的平衡因子为 2,失衡了。
这属于在右子树的左子树上插入节点之后导致的失衡,此时需要先让 p 节点右旋转,然后让 g 节点左旋转
旋转过程如下:
添加导致的失衡实现部分代码如下:
private void rebalance(Node<E> grand) {
Node<E> parent = ((AVLNode<E>)grand).tallerChild();
Node<E> node = ((AVLNode<E>)parent).tallerChild();
if (parent.isLeftChild()) { // L
if (node.isLeftChild()) { // LL
rotateRight(grand);
} else { // LR
rotateLeft(parent);
rotateRight(grand);
}
} else { // R
if (node.isLeftChild()) { // RL
rotateRight(parent);
rotateLeft(grand);
} else { // RR
rotateLeft(grand);
}
}
}
四、删除导致的失衡
作为一颗二叉树搜索树,AVL 树的删除跟二叉树搜索树的删除一致
叶子节点 | 度为 1 的节点 | 度为 2 的节点 |
直接删除 | 使用子节点替代原节点的位置 | 先用前驱或者后继节点的值覆盖原节点的值,然后删除相应的前驱或者后继节点 |
以下我们只讨论度为 2 的节点的删除,如下图所示:删除子树中的16
可能会导致 父节点 或 祖先节点 失衡,其他节点都不会失衡
同理,删除导致的失衡也是通过旋转回复平衡
如下图所示:删除红框节点
如果没有 绿色框节点,p右旋之后,右边的子树高比左边的子树高小1。
可能会导致 p 节点的父节点或更高层节点失衡。
极端情况下,所有祖先节点都需要进行恢复平衡的操作,共 O(logn)次调整。
五、总结
添加 | 删除 | 搜索 | |
时间复杂度 | O(logn) | O(logn) | O(logn) |
失衡调整 | 可能会导致所有祖先节点都失衡,只要让高度最低的失衡节点恢复平衡,整颗树就平衡了,仅需要O(1)次调整 | 可能会导致父节点或祖父节点失衡(只有一个节点会失衡),恢复平衡后可能会导致更高层的祖先节点失衡,最多需要 O(logn) 次调整 |