一、定义
AVL树即为带平衡条件的二叉搜索树。在保留二叉搜索树特性的同时,又加上了平衡条件,即每个节点的左右子树的高度最多差1。
AVL树的存储结构如下:
typedef struct avlnode {
int data;
struct avlnode* left;
struct avlnode* right;
int height;
}avlnode;
本文主要讨论AVL树的插入操作。
二、旋转
1.旋转操作
所谓的旋转操作是保证AVL树平衡条件的一种操作方法。当我们插入新节点的时候,有可能破坏AVL树原有的平衡条件。我们记必须重新平衡的节点为a。由于一个节点最多有两个子节点,因此树不平衡时,两棵子树的高度最多差2。不难发现不平衡会出现在以下四种情况之中:
(1).对a左子节点的左子树进行插入。
(2).对a左子节点的右子树进行插入。
(3).对a右子节点的左子树进行插入。
(4).对a右子节点的右子树进行插入。
其中(1)(4)镜像对称,(2)(3)镜像对称。所以理论分析上我们只有两种情况。当然,我们写程序时还要分4种情况。
2.单旋转操作
当插入节点在“外侧”时(即左-左或右-右的情况),进行所谓的单旋转操作。下图为左-左的情况示意图。在示意图中,在同一虚线上代表在同一层。
我们可以把这个过程形象的想象为抓住节点k1,使劲“抖动”,使Y子树连接到k2上。这样就完成了子树的调整。由于子树的高度差最多为2,这样调整时可以满足AVL树的平衡条件的。
现在我们写出左-左旋转的代码。在写代码时,务必保持头脑清醒,最好对着画好的图来写。
//左-左,单旋转函数:左外侧子树长
avlnode* singlerotateleft(avlnode* root) {
//更新节点的链接
avlnode* root1 = root->left;
root->left = root1->right;
root1->right = root;
//更新树的深度值
root->height = umax(Height(root->left), Height(root->right)) + 1;//注意这里的加一
root1->height = umax(Height(root1->left), Height(root1->right)) + 1;
return root1;
}
对于右-右的情况,我们也可以相似处理:
代码:
//右-右,单旋转函数:右外侧子树长
avlnode* singlerotateright(avlnode* root) {
avlnode* root1 = root->right;
root->right = root1->left;
root1->left = root;
root->height = umax(Height(root->left), Height(root->right)) + 1;
root1->height = umax(Height(root1->left), Height(root1->right)) + 1;
return root1;
}
3.双旋转操作
当插入节点在“内侧”的时候(如左-右或右-左情况),一次单旋转是无法完成修正的(如图4-34)
如何解决这一问题呢?这时,我们要将Y看成一个节点和两棵子树,在将两个子树分别放到左右两边(如图4-35)
我们可以把双旋转看成两个单旋转,即先将k1,k2进行一次单旋转,在对k3,k2进行一次单旋转。
按照这个想法,我们可以写出代码:
//左-右双旋转函数:左内侧子树长 root-k3,root1-k1,root2-k2
avlnode* doublerotateleft(avlnode* k3) {
avlnode* k1 = k3->left, * k2 = k3->left->right;
将k1和k2旋转,注意是右-右单旋转,将旋转完的结果作为root的左子节点
k3->left = singlerotateright(k1);
将k3和k2旋转,注意是左-左单旋转,将旋转完的结果返回
return singlerotateleft(k3);
}
相似的,我们也可以写成右-左双旋转:
//右-左,双旋转函数:右内侧子树长
avlnode* doublerotateright(avlnode* k3) {
k3->right = singlerotateleft(k3->right);
return singlerotateright(k3);
}
三、插入节点操作
在有了以上对旋转的讨论之后,完成节点的插入操作就不再是难事。
总体上插入函数的思路与二叉搜索树的插入函数思路相似,不过多了对树高导致不平衡的条件的讨论。
//插入函数
void insert(avlnode** root, int x) {
if (*root == NULL) {
*root = (avlnode*)malloc(sizeof(avlnode));
(*root)->data = x;
(*root)->height = 0;
(*root)->left = (*root)->right = NULL;
}
else {
if ((*root)->data > x) {
insert(&(*root)->left, x);
if (Height((*root)->left) - Height((*root)->right) == 2) {
//高度差等于二,树不再平衡
if (x < (*root)->left->data) {//在“外侧”
(*root) = singlerotateleft((*root));
}
else {//在“内侧”
(*root) = doublerotateleft((*root));
}
}
}
else if ((*root)->data < x) {
insert(&(*root)->right, x);
if (Height((*root)->right) - Height((*root)->left) == 2) {
if (x > (*root)->right->data) {//在“外侧”
(*root) = singlerotateright((*root));
}
else {//在“内侧”
(*root) = doublerotateright((*root));
}
}
}
//如果相等,那么不必操作
}
(*root)->height = umax(Height((*root)->left), Height((*root)->right)) + 1;更新高度
}
不过值得注意的是,代码中的Height函数单独出来写是有必要的,因为我们要处理空树的情况。单独出来写代码更加简洁。
int Height(avlnode* root) {
if (root == NULL) return -1;//我们将空树的高度定义为-1
return root->height;
}
三、AVL树节点的删除操作
《数据结构与算法分析》一书中认为AVL树的删除操作较为复杂,更加推荐使用懒惰删除,即保留节点在树中,但是加以标记。
如果按正常的思路完成AVL树节点的删除也不是不行,但总共有12种情况。个人认为以下两篇文章对删除操作解释比较清楚:
关于删除的图解:
参考资料:《数据结构与算法分析:C语言描述》