本文不是入门篇,零基础请绕过。
先用维基上的话来介绍一下基本概念。
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G.M. Adelson-Velsky和E.M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。
节点的平衡因子是它的左子树的高度减去它的右子树的高度。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。
所谓失去平衡的最小子树,是以距离插入结点最近的、且平衡因子绝对值大于1的结点作为根的子树。我们下面说的旋转,就是要旋转这个最小子树。
当AVL树不平衡时,有以下四种情况:LL、LR、RR、RL。
针对四种种情况可能导致的不平衡,可以通过旋转使之变平衡。有两种基本的旋转:
(1)左旋转:将根节点旋转到(根节点的)右孩子的左孩子位置
(2)右旋转:将根节点旋转到(根节点的)左孩子的右孩子位置
有人也把左旋转叫做逆时针旋转,右旋转叫做顺时针旋转。
下面结合代码来说一下。
typedef struct Node *NodePtr, *AVL_Tree;
struct Node{
NodePtr left;
NodePtr right;
int height;
int data;
Node(int data){
this->data = data;
height = 0;
right = nullptr;
left = nullptr;
}
};
其实后来想想,在结点的结构中应该加入指向父结点的指针,这样,就能避免AVL树 用递归来进行插入和删除结点了。也就是类三叉链表结构。
1. LL情况
采用右旋方法来解决 失衡问题。
从上图我们看到,最小子树的根,即失衡结点为A,采用右旋,将A旋转到A的左孩子的右孩子位置。可以形象地看出,结点A绕着自己的左孩子B,向右顺时针旋转到了当前位置。
//将根节点旋转到(根节点的)左孩子的右孩子位置
NodePtr RightRotate(AVL_Tree TreeRoot) {
NodePtr lchild = TreeRoot->left;
TreeRoot->left = lchild->right;
lchild->right = TreeRoot;
TreeRoot->height = Max(TreeRoot->left->height, TreeRoot->right->height);
lchild->height = Max(lchild->left->height, lchild->right->height);
return lchild;
}
2. RR情况
采用左旋转的方法解决。
从上图可以看出,最小子树的根为A,采用左旋转方法,即向左 逆时针旋转。将根节点旋转到(根节点的)右孩子的左孩子位置。可以形象地看出,结点A绕着自己的右孩子B,向左逆时针旋转到了当前位置。
//将根节点旋转到(根节点的)右孩子的左孩子位置
NodePtr LeftRotate(AVL_Tree TreeRoot) {
NodePtr rchild = TreeRoot->right;
TreeRoot->right = rchild->left;
rchild->left = TreeRoot;
aTreeRoot->height = Max(aTreeRoot->left->height, TreeRoot->right->height);
rchild->height = Max(rchild->left->height, rchild->right->height);
return rchild;
}
3. LR情况
LR情况需要两次旋转,先左旋,再右旋。要先 以失衡点的左孩子为根进行左旋,再以失衡点为根进行右旋。
NodePtr LeftRightRotate(AVL_Tree TreeRoot) {
TreeRoot->left = LeftRotate(TreeRoot->left);
return RightRotate(TreeRoot);
}
4. RL情况
RL情况也需要两次旋转,先右旋,再左旋。要先以失衡点的右孩子为根进行右旋,再以失衡点为根进行左旋。
NodePtr RightLeftRotate(AVL_Tree TreeRoot) {
TreeRoot->right = RightRotate(TreeRoot->right);
return LeftRotate(TreeRoot);
}
AVL树的插入与删除
凡是二叉查找树,或者叫做二叉排序树(BST),所有新插入的关键字均存储在新创建的叶子结点上。因为AVL是也是一种特殊的BST,故它的插入的新结点,也都是插入到叶子结点处。再进行旋转调整平衡。
1.插入
//将整型数据newData插入 AVL树 TreeRoot中
NodePtr Insert(int newData, AVL_Tree TreeRoot)
{
if(TreeRoot == NULL)
{
TreeRoot = new Node(newData);
return TreeRoot;
}
else if(newData < TreeRoot->data)
{
TreeRoot->left = Insert(newData, TreeRoot->left);
//已插入完毕,且TreeRoot以下结点已调整完毕
//因为插入在左子树上,所以如果失衡,则左子树的高度一定大于右子树
if(TreeRoot->left->height - TreeRoot->right->height == 2)
{
if(newData < TreeRoot->left->data)
{
//插入在左子树上,且插入的值比左子树的根小
//所以是LL型失衡
TreeRoot = RightRotate(TreeRoot);
}
else
{
//插入在左子树上,且插入的值比左子树的根大
//所以是LR型失衡
TreeRoot = LeftRightRotate(TreeRoot);
}
}
}
else
{
TreeRoot->right = Insert(newData, TreeRoot->right);
//已插入完毕,且TreeRoot以下结点已调整完毕
//因为插入在右子树上,所以如果失衡,则右子树的高度一定大于左子树
if(TreeRoot->right->height - TreeRoot->left->height == 2)
{
if(newData > TreeRoot->right->data)
{
//插入在右子树上,且插入的值比左子树的根大
//所以是RR型失衡
TreeRoot = LeftRotate(TreeRoot);
}
else
{
//插入在右子树上,且插入的值比左子树的根小
//所以是RL型失衡
TreeRoot = RightLeftRotate(TreeRoot);
}
}
}
TreeRoot->height = Max(TreeRoot->left->height, TreeRoot->right->height) + 1;
return TreeRoot;
}
2. 删除操作
把待删除的结点及其所有孩子看做一棵子树,待删除的结点为这棵树的根。
删除操作比较复杂,这里只说一下原理,不写代码了。
1)如果待删除的结点是叶子结点,那么直接删除该叶子结点,并将其父结点对应指针置nullptr,同时,从下向上依次调整该条路径上的平衡。这是最简单的情况
2)当待删除的结点只有一棵子树的时候,将待删除结点的父结点对应的指针指向该子树,之后,删除该结点,同时,从该父结点开始,从下向上依次调整该条路径上的平衡。
3)当待删除的结点有左右两棵子树时,找到该结点的相邻关键字。然后,将该关键字的值赋值给待删除的结点。这样,就把问题转化为删除该关键字所在的叶子结点问题了。即第1)种情况。
解释一下什么叫做相邻关键字。对于不在叶子结点上的关键字a,它的相邻关键字为其左子树中的最大值或 其右子树中的最小值。从该定义可以清晰地看出,相邻关键字一定位于叶子结点处。更直白地说,相邻关键字就是,沿着a的左指针来到其左子树的根结点,然后沿着右指针一下往右走,直到叶子结点为止。 或者是, 沿着a的右指针来到其右子树的根结点,然后沿着左指针一下往左走,直到叶子结点为止。