普通二叉排序树的缺点
之前讲过二叉排序树, 即某个树的任意一个节点,都满足左子树的值都小于该节点值,右子树的值都大于该节点的值。
同样的数字集合,按照不同的方式插入一个新的二叉排序树时,会形成不同的树结构。
例如按照:1, 2, 3, 4, 5 的顺序插入二叉排序树,则正常操作下二叉排序树会退化成一个只有右节点的链表。
链表结构对于查找,删除等操作都是不方便操作的,平均时间复杂度都是O(N)。这就是普通二叉排序树的缺点。
AVL树简介
来源与概念
AVL树是一种特殊的二叉排序树。
名称:AVL树
发明者:
G. M. Adelson-Velsky
E. M. Landis
年代:1962年(60年历史)
性质:对于任意一个节点,左子树和右子树的高度差绝对值都不大于1。
| H(left) - H(right) | <= 1
优点:
由于对每个节点的左右子树的树高做了限制,所以整棵树不会退化成一个链表。
AVL树-左旋操作介绍
抓住K1节点,向左旋转:K1成了K3的左子树,K3原来的左子树成了K1的右子树。
可以看出,如果左旋之前是二叉排序树,那么左旋操作以后仍然是二叉排序树。
AVL树-右旋操作介绍
右旋是左旋的逆操作,如下图抓住K1右旋:K1成了K2的右子树,K2原来的右子树成了K1的左子树。
可以看出,如果右旋之前是二叉排序树,那么右旋操作以后仍然是二叉排序树。
AVL树-失衡类型及调整方案
K1节点:插入与删除的过程中发现的第一个失衡的节点。
共分四种类型:LL型,LR型,RL型, RR型。
例如LL:K1节点往下看时第一次发现失衡,且左子树更高,左子树的左子树更高。注意K1往下的K2, K3等都没有发现失衡。
LR型为K1的左子树的右子树更高,依次类推。
当出现LL型失衡时,A和B两个节点一定A比B高。因为节点是一个一个插入的。其他失衡类型也类似,对应的两个孙子节点一定有一个高一个低。
可以看出,LL和RR类型本质可以归为一大类,LR和RL类型可以归为一大类。
AVL树-LL型失衡
抓着K1进行一个大右旋。
为什么右旋以后就会变成AVL树?
首先上面分析过,A节点的高度一定比B节点的高度多一个。
从右旋之前来看,K2, K3两个节点的高度分别为:
h
K
2
=
h
A
+
1
(1)
h_{K2} = h_A + 1\tag{1}
hK2=hA+1(1)
h
K
3
=
max
(
h
C
,
h
D
)
+
1
(2)
h_{K3} = \max(h_C, h_D) + 1\tag{2}
hK3=max(hC,hD)+1(2)
又因为LL失衡,所以:
h
K
2
=
h
K
3
+
2
(3)
h_{K2} = h_{K3} + 2\tag{3}
hK2=hK3+2(3)
将(1)式和(2)式分别带入(3)式,可得:
h
A
=
max
(
h
C
,
h
D
)
+
2
(4)
h_A = \max(h_C, h_D) + 2\tag{4}
hA=max(hC,hD)+2(4)
即
h
A
=
h
B
+
1
=
max
(
h
C
,
h
D
)
+
2
(5)
h_A = h_B + 1 = \max(h_C, h_D) + 2\tag{5}
hA=hB+1=max(hC,hD)+2(5)
所以从A,B,C,D四个节点的关系看右旋后的树, 从K3节点向下看只有C和D两个子树,没失衡;
从K1节点向下看,根据(5)式,
h
B
=
max
(
h
C
,
h
D
)
+
1
=
h
K
3
h_B = \max(h_C, h_D) + 1 = h_{K3}
hB=max(hC,hD)+1=hK3, 所以也没有失衡;
从K2节点向下看,
h
A
=
h
B
+
1
=
h
K
1
h_{A} = h_{B} + 1 = h_{K1}
hA=hB+1=hK1, 也没有失衡。
所以右旋后,无论从哪个节点看,都维持了AVL树的性质。
AVL树-LR型失衡
先抓住K2进行一个小左旋,将其变为LL型失衡,再抓住K1进行一个大右旋。
经过这样的调整后会变成一个巨平衡的树。证明如下:
首先看调整之前,K3的高度可以表示为:
h
K
3
=
max
(
h
B
,
h
C
)
+
1
(6)
h_{K3} = \max(h_B, h_C) + 1\tag{6}
hK3=max(hB,hC)+1(6)
又因为之前说过,出现LR失衡时K3的高度一定比A高度高1个,即:
h
K
3
=
h
A
+
1
(7)
h_{K3} = h_A + 1\tag{7}
hK3=hA+1(7)
结合(6)和(7)可得:
h
A
=
max
(
h
B
,
h
C
)
(8)
h_{A} = \max(h_B, h_C)\tag{8}
hA=max(hB,hC)(8)
K2的高度:
h
K
2
=
h
K
3
+
1
=
h
A
+
2
(
K
3
比
A
高一个
)
=
h
D
+
2
(
从
K
1
向下看失衡了
)
(9)
\begin{aligned} h_{K2} &= h_{K3} + 1 \\ &= h_A+2 \ \ \ \ \ \ \ \ (K3比A高一个) \\ &= h_D + 2 \ \ \ \ \ \ \ \ (从K1向下看失衡了) \end{aligned} \tag{9}
hK2=hK3+1=hA+2 (K3比A高一个)=hD+2 (从K1向下看失衡了)(9)
所以可以得到
h
A
=
h
D
(10)
h_A = h_D\tag{10}
hA=hD(10)
即A,B,C,D的高度关系为:
h
A
=
h
D
=
max
(
h
B
,
h
C
)
(11)
h_A = h_D = \max(h_B, h_C)\tag{11}
hA=hD=max(hB,hC)(11)
所以用这个关系看调整后的每个节点,可以发现从K1, K2, K3向下看都是平衡的。
小练习:
按照如下顺序插入数字,画出对应的AVL树。
1 : [ 5, 9, 8, 3, 2, 4, 1, 7 ]
2:[ 1, 2, 3, 4, 5 ]
答案略。
RR类型和LL类型类似,进行一个大左旋即可;
RL类型和LR类型类似,先将右子树进行一个小右旋,再将失衡的当前节点进行一个大左旋即可。
AVL树的代码演示
#include <iostream>
#include <vector>
#include <cstdio>
using namespace std;
//虚拟空节点,为避免后续各种为空的判断,引入之
#define NIL (&Node::__NIL)
struct Node {
Node(int key = 0, int h = 0, Node *left = NIL, Node *right = NIL)
:key(key), h(h), left(left), right(right) {}
int key, h;
Node *left, *right;
static Node __NIL; //虚拟空节点静态变量
};
Node Node::__NIL; //虚拟空节点变量声明
Node *getNewNode(int key) {
return new Node(key, 1);
}
//调整树的高度
void update_height(Node *root) {
root->h = max(root->left->h, root->right->h) + 1;
return;
}
//左旋操作
Node *left_rotate(Node *root) {
Node *temp = root->right;
root->right = temp->left;
temp->left = root;
update_height(root);
update_height(temp);
return temp;
}
//右旋操作
Node *right_rotate(Node *root) {
Node *temp = root->left;
root->left = temp->right;
temp->right = root;
update_height(root);
update_height(temp);
return temp;
}
//二叉树的高度调整
Node *maintain(Node *root) {
if (abs(root->left->h - root->right->h) < 2) return root;
if (root->left->h > root->right->h) {
if (root->left->right->h > root->left->left->h) {
root->left = left_rotate(root->left);
}
root = right_rotate(root);
}else {
if (root->right->left->h > root->right->right->h) {
root->right = right_rotate(root->right);
}
root = left_rotate(root);
}
return root;
}
//AVL 树的插入操作
Node *insert(Node *root, int key) {
if (root == NIL) return getNewNode(key);
if (key == root->key) return root;
else if (key < root->key) root->left = insert(root->left, key);
else if (key > root->key) root->right = insert(root->right, key);
update_height(root);
return maintain(root);
}
//AVL树寻找节点的前驱
Node *predecessor(Node *root) {
Node *temp = root->left;
while (temp->right != NIL) temp = temp->right;
return temp;
}
//AVL 树的删除操作
Node *erase(Node *root, int key) {
if (root == NIL) return root;
if (key < root->key) root->left = erase(root->left, key);
else if (key > root->key) root->right = erase(root->right, key);
else {
if (root->left == NIL || root->right == NIL) {
Node *temp = root->left == NIL ? root->right : root->left;
delete root;
return temp;
}else {
Node *temp = predecessor(root);
root->key = temp->key;
root->left = erase(root->left, temp->key);
}
}
update_height(root);
return maintain(root);
}
//删除一棵树
void clear(Node *root) {
if (root == NIL) return ;
clear(root->left);
clear(root->right);
cout << "delete : " << root->key << endl;
delete root;
return;
}
//打印节点信息
void print(Node *root) {
printf("(%d[%d]) | %d %d\n", root->key, root->h, root->left->key, root->right->key);
}
//输出一棵树
void output(Node *root) {
if (root == NIL) return;
print(root);
output(root->left);
output(root->right);
return ;
}
int main() {
int op, val;
Node *root = NIL;
while (cin >> op >> val) {
switch(op) {
case 0: root = insert(root, val); break;
case 1: root = erase(root, val); break;
}
cout << endl << "===== AVL tree print ======" << endl;
output(root);
cout << endl << "===== tree print done ======" << endl;
}
clear(root);
return 0;
}
总结:1. 引入虚拟空节点,减少了大量判断的操作,使得实际的调整代码较为简洁;2. 递归插入,递归删除;3. 合并某些重复的代码。