在上一篇 二叉搜索树的介绍中,我们已经证明了高度为 h h h 的二叉搜索树上的每个基本操作都可以在 O ( h ) O(h) O(h) 时间内完成,然而随着元素的插入和删除,二叉搜索树的高度是变化的。例如,如果 n n n 个关键字按照严格递增的次序被插入,则这棵树一定是高度为 n n n 的一条链,在这样的树上进行操作的时间性能是比较低的。也就是说, 如果搜索树的高度较低时,这些集合操作会执行得较快;然而如果树的高度较高时,这些集合操作可能并不比在链表上执行的快。也就有了各种各样的 “平衡”搜索树。
AVL 树是最早的自平衡二叉树,相比于后来出现的平衡二叉树(红黑树,treap,splay树)而言,它现在应用较少,但研究 AVL 树对于了解后面出现的常用平衡二叉树具有重要意义。
AVL 树定义
AVL 树或者是空二叉树,或者是具有如下性质的 BST(Binary Search Tree,二叉搜索树):
- 根结点的左、右子树高度之差的绝对值不超过 1;
- 根结点左子树和右子树仍然是 AVL 树。
结点的平衡因子 BF(Balanced Factor)
指一个结点的左子树与右子树的高度之差。
AVL 树中的任意节点的 BF 只可能是 − 1 , 0 , 1 -1,0,1 −1,0,1。
AVL树的平衡化处理
AVL树在结点高度上采用相对平衡的策略,使其平均性能接近于 BST 的最好情况的性能。
AVL 树的查找操作与二叉搜索树相同,之所以其 ASL(Average Search Length) 可保持在 O ( l o g 2 n ) O(log_2n) O(log2n),因为向 AVL 树插入、删除节点可能造成不平衡,此时调整了树的结构,使之重新达到平衡。
AVL 树种查找、插入和删除在平均和最坏情况下都是 O ( l o g 2 n ) O(log_2n) O(log2n),增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
有四种种情况可能导致二叉查找树不平衡,分别为:
(1)LL:插入一个新节点到根节点的左子树(Left)的左子树(Left),导致根节点的平衡因子由 1 变为 2
(2)RR:插入一个新节点到根节点的右子树(Right)的右子树(Right),导致根节点的平衡因子由 -1 变为 -2
(3)LR:插入一个新节点到根节点的左子树(Left)的右子树(Right),导致根节点的平衡因子由 1 变为 2
(4)RL:插入一个新节点到根节点的右子树(Right)的左子树(Left),导致根节点的平衡因子由 -1 变为 -2
针对四种种情况可能导致的不平衡,可以通过旋转使之变平衡。有两种基本的旋转:
(1)左旋转:将根节点旋转到(根节点的)右孩子的左孩子位置
(2)右旋转:将根节点旋转到(根节点的)左孩子的右孩子位置
基本数据结构:
typedef struct Node* Tree;
typedef struct Node* Node_t;
typedef Type int;
struct Node{
Node_t left;
Node_t right;
int height; // 平衡因子
Type data;
};
int Height(Node_t node) {
return node->height;
}
LL 型
LL 型需要右旋转解决(如图,绕 B 将 A 右旋转)。
Node_t RightRotate(Node_t a) {
b = a->left;
a->left = b->right;
b->right = a;
a->height = Max(Height(a->left), Height(a->right));
b->height = Max(Height(b->left), Height(b->right));
return b;
}
RR 型
RR 型需要左旋转解决(如图,绕 C 将 A 左旋转)。
Node_t LeftRotate(Node_t a) {
b = a->right;
a->right = b->left;
b->left = a;
a->height = Max(Height(a->left), Height(a->right));
b->height = Max(Height(b->left), Height(b->right));
return b;
}
LR 型
LR 型需要左右旋转解决(如图,先绕 E 将 B 左旋转,再绕 E 将 A 右旋转)。
Node_t LeftRightRotate(Node_t a) {
a->left = LeftRotate(a->left);
return RightRotate(a);
}
RL 型
RL 型需要右左旋转解决(如图,先绕 D 将 C 右旋转,再绕 D 将 A 右旋转)。
Node_t RightLeftRotate(Node_t a) {
a->right = RightRotate(a->right);
return LeftRotate(a);
}
AVL 树插入操作
插入操作实际上就是在不同情况下采用不同的旋转方式调整整棵树:
- 首先按照标准的二叉搜索树进行元素插入操作;
- 然后检查这次操作是否破坏了树的平衡,若是,判断其不平衡的类型,进行旋转修复。
假设 25, 27, 30, 12, 11, 18, 14, 20, 15, 22
是一关键字序列,并以上述顺序建立 AVL 树。
25, 27, 30, 12, 11, 18, 14, 20, 15, 22
25, 27, 30, 12, 11, 18, 14, 20, 15, 22
25, 27, 30, 12, 11, 18, 14, 20, 15, 22
25, 27, 30, 12, 11, 18, 14, 20, 15, 22
25, 27, 30, 12, 11, 18, 14, 20, 15, 22
25, 27, 30, 12, 11, 18, 14, 20, 15, 22
AVL 树的删除操作
在一棵树中,删除某个元素,逻辑应该是这样子的:
- 首先按照标准的二叉搜索树进行元素删除操作;
- 然后检查这次操作是否破坏了树的平衡,若是,判断其不平衡的类型,进行旋转修复。
删除与插入操作是对称的(镜像,互逆的):
- 删除右子树结点导致失衡时,相当于在左子树插入导致失衡,即 LL 或 LR;
- 删除左子树结点导致失衡时,相当于在右子树插入导致失衡,即 RR 或 RL;
删除操作可能需要多次平衡化处理:
- 因为平衡化不会增加子树的高度,但可能会减少子树的高度。
- 在有可能使树增高的插入操作中,一次平衡化能抵消掉树增高;
- 而在有可能使树减低的删除操作中,平衡化可能会带来祖先结点的不平衡。
- 因此,删除操作可能需要多次平衡化处理。