AVL树是一种带有自平衡性质的的二叉查找树,也就是说,在多次插入删除或者极端数据情况下依然可以保证O(logn)的操作复杂度。文章同步发布在个人博客,链接:AVL树。
自平衡的二叉查找树
上一篇中的二叉查找树在多次插入删除后,树的节点会向其中一边下沉,操作(插入/删除/查找)的复杂度会从O(logn)渐渐增大。在数据量很大时,操作时间可能变得不可接受。而且当建树的一组数据是已经排好序的极端情况时,普通的二叉查找树就会退化为代价高昂的链表,而时间复杂度将会退化为线性的O(n)。
而要避免以上问题,我们需要为二叉查找树建立一个平衡的附加结构条件。条件不同,实现不同。当附加平衡条件后,二叉树便可以实现自平衡,即当大量插入删除时仍然可以保持O(logn)的最坏操作时间复杂度,我们将其称作平衡二叉搜索树(Self-balancing binary search tree)。平衡二叉树常见的实现有AVL树、红黑树(Red Black Tree)、伸展树(Splay tree)等。工程中大量使用红黑树,以后有机会讨论。这里简要实现AVL树。
AVL Tree
AVL树是以它的发明者(G.M. Adelson-Velsky 和 E.M. Landis)来命名的,也是最先发明的自平衡二叉查找树,于1962年发明。
AVL树的平衡条件是:每个节点的左子树与右子树高度相差不超过1。其中只有一个根节点的高度定义为0,空树高度定义为-1。
这里需要区分两个概念,节点的深度与高度,深度是从根节点到该节点的唯一路径的长度,高度是从该节点到其后代的树叶的最长路径。则根节点深度为0,对一棵树来说,树的高等于根节点的高,也等于该树最深的树叶的深度,即树的深度。
因为插入和删除可能会破坏AVL树的平衡条件,所以插入和删除后如果失衡则必须对其进行修正/平衡化。我们称其为旋转。
首先考虑插入,我们把必须要重新平衡的节点就做a,因此高度不平衡时,a的左子树与右子树高度相差2。这可以分为以下4种情况:
- 对a的左儿子的左子树进行了一次插入。
- 对a的左儿子的右子树进行了一次插入。
- 对a的右儿子的左子树进行了一次插入。
- 对a的右儿子的右子树进行了一次插入。
情形1和4是对称的,2和3也是对称的。因为需要通过节点的高度来判断是否平衡,所以我们在节点中增加一个域height来表示该节点的高度。所以节点定义如下。
#define Max(a,b) (a>b?a:b)
typedef int ElemType;
typedef struct TreeNode
{
ElemType element;
TreeNode * left;
TreeNode * right;
int height;
} * pNode, * AvlTree;
操作声明:
int Height(pNode p);
void Insert(AvlTree & T,ElemType x);
void Delete(AvlTree & T,ElemType x);
pNode Single_rotate_left(pNode p); //从左向右单旋
pNode Single_rotate_right(pNode p);//从右向左单旋
pNode Double_rotate_left(pNode p);
pNode Double_rotate_right(pNode p);
AvlTree CreateTree(ElemType A[],int n);//从数组建树
由于需要通过计算高度来判断平衡,所以必须定义一个计算节点高度的函数:
int Height(pNode p)
{
if(p == NULL)
return -1;
else
return p->height;
}
从数组建树:
AvlTree CreateTree(ElemType A[],int n)
{
AvlTree T = NULL;
for(int i = 0; i < n; i++)
Insert(T,A[i]);
return T;
}
单旋转
考虑情形1,如果对a的左儿子的左子树进行了一次插入,导致a的左子树比右子树高度大2的话,那么我们可以通过如下的单旋转来使其重新平衡。
旋转后,返回tmp,很容易看出旋转后,依然满足二叉查找树的性质,而且重新达到了平衡。这一过程的实现:
pNode Single_rotate_left(pNode p)
{
pNode tmp = p->left;
p->left = tmp->right;
tmp->right = p;
p->height = Max(Height(p->left),Height(p->right)) + 1;
tmp->height = Max(Height(tmp->left),p->height) + 1;
return tmp;
}
类似的情形4,则如图所示。
实现(与情形1镜像对称):
pNode Single_rotate_right(pNode p)
{
pNode tmp = p->right;
p->right = tmp->left;
tmp->left = p;
p->height = Max(Height(p->left),Height(p->right)) + 1;
tmp->height = Max(Height(tmp->right),p->height) + 1;
return tmp;
}
上面的实现中,Single_rotate_left
表示的是从左向右单旋,Single_rotate_right
是从右向左单旋。
双旋转
情形2和3则无法通过单旋转实现平衡,但可以通过一次双旋转来完成。就情形2来说,对a的左儿子的右子树进行了一次插入导致a的左子树比右子树高度大了2。则可以通过如下的旋转使得再次达到平衡。
可以很明显的看出双旋转可以通过两次单旋转来完成,上图中,首先对子树k1进行可一次从右向左的单旋,然后对k3进行了一次从左向右的单旋。所以实现时调用上述函数即可。
pNode Double_rotate_left(pNode p)
{
p->left = Single_rotate_right(p->left);
return Single_rotate_left(p);
}
类似,情形3,如下。
实现:
pNode Double_rotate_right(pNode p)
{
p->right = Single_rotate_left(p->right);
return Single_rotate_right(p);
}
上面的1,2,3,4四种情形也经常因为插入位置被称为LL,LR,RL,RR。
一个描述了这几种旋转的动图:
插入
旋转实现了,则插入就可以实现了,注意旋转中必须对节点高度进行必要的更新。AVL树插入节点的实现:
void Insert(AvlTree & T,ElemType x)
{
if(T == NULL)
{
T = new TreeNode;
T->element = x;
T->height = 0;
T->left = T->right = NULL;
}
else if(x < T->element)
{
Insert(T->left,x); //在T的左子树插入
if(Height(T->left) - Height(T->right) == 2) //左子树失衡,需要平衡
{
if(x < T->left->element) //插入在T的左孩子的左子树,单旋转,情形1
T = Single_rotate_left(T);
else //插入在T的左孩子的右子树,双旋转,情形2
T = Double_rotate_left(T);
}
}
else if(x > T->element)
{
Insert(T->right,x); //在T的右子树插入
if(Height(T->right) - Height(T->left) == 2) //右子树失衡,需要平衡
{
if(x > T->right->element) //插入在T的右孩子的右子树,单旋转,情形4
T = Single_rotate_right(T);
else //插入在T的右孩子的左子树,双旋转,情形3
T = Double_rotate_right(T);
}
}
//else x is aleardy exist in the tree, we'll do nothing.
//Don't forget to update the height of root
T->height = Max(Height(T->left),Height(T->right)) + 1;
}
删除
比较复杂,暂时未实现。对于这种情况,采用懒惰删除也许会是一个好的选择,节点中添加一个域,查找到后将其状态修改为deleted就Ok。略。
源码
点击下载。
参考资料:数据结构与算法分析——C语言描述