平衡二叉树(AVL树)
定义:
AVL树是一颗特殊的二叉查找树,它具有保持平衡要求的特性,AVL树的每一个节点的左子树和右子树的高度差要求最大为1,如果超过1将破坏平衡特性,将不再是一颗平衡二叉树。
平衡条件的维持:
为什么需要维持平衡:
AVL树需要时刻保持其平衡的特性,但这样的特性可能会在插入和删除的同时被打破,插入和删除新的节点造成的高度变化可能会使某些节点的左右子树的高度差超过1,所以我们需要在插入和删除的同时维护AVL树的平衡特性。
讨论如何维持AVL树的平衡:
- 明确平衡被破坏的位置
在讨论AVL树的平衡维护之前,稍微提及之前二叉查找树插入或删除节点后,沿变动节点至根部为保持排序特性做了一系列上浮或者下滤操作。也就是说平衡特性的改变只会发生在这一路径的某些相关节点上。 - 找到需要进行操作的关键节点
我们为了让被打破平衡的AVL树重新平衡,需要做的第一间事情是找到这样一个特殊的节点,我们知道可能被打破平衡的节点不止一个,但是我们需要找到的特殊节点是最深处的被打破平衡的节点,我们称之为a节点。
赘述以下a节点的特征:
1.以a节点为根的树不再具有AVL树的平衡特性,即就是说a节点的左子树和右子树的高度差大于1.
2.在众多具有a节点第1个特征的节点中,a节点具有最大的深度,也就是a节点是距离根最远的这样一个节点。 - 树的旋转操作
针对插入操作导致的不平衡无非发生在下面四种情况之一:
1.对a的左儿子的左子树进行了一次插入
2.对a的左儿子的右儿子进行了一次插入
3.对a的右儿子的左儿子进行了一次插入
4.对a的右儿子的右儿子进行了一次插入
用下面比较丑的四个图来大概描述一下四种情形:
我们针对这四种不平衡的情形,都将采取名为旋转的操作来恢复AVL树的平衡,只是根据情形的不同,我们又将分别使用单旋转和双旋转来实现。
1.单旋转:
使用单旋转进行恢复是相对简单的情况,这种情况包括了上面的情形1和4,形象地来讲就是位于最外侧路径的插入导致的不平衡的情形。
针对于情形1导致的不平衡的情形,可以通过以下单旋转的过程来恢复:
将上图所示单旋转过程可以简单描述为:
形象地来讲,我们可以通过将k1节点提起,让k2节点(a节点)下落旋转来消除这种高度差。
其操作可以简单地描述为:使k1节点右儿子成为k2节点的左儿子,然后让k2节点成为k1节点的右儿子,即可完成旋转。如果是在代码的实现细节中,我们最后还需要返回k1节点作为新的根节点来替代之前的k2节点。
如果是4情形,那么正好和上述的1情形镜像对称,不再赘述。
下面先给出单旋转的c代码实现:
AVLTree rotateWithLeftChild(AVLTree k2)//左子树单旋转
{
AVLTree k1=k2->left;//转轴k1是k2的左儿子
k2->left=k1->right;//让k1的右节点成为k2的左儿子
k1->right=k2;//让k2成为k1的右儿子
//重新计算k1和k2的高度
k1->height=Height(k1->left)>=Height(k1->right)?Height(k1->left)+1:Height(k1->right)+1;
k2->height=height(k2->left)>=Height(k2->right)?Height(k2->left)+1:Height(k2->right)+1;
}
2双旋转:
针对于情形2和3我们会无奈地发现只依靠单旋转是无法解决问题的
比如下图所示情形2使用单旋转确实是无法恢复平衡的:
于是我们将引出双旋转以解决情形2和3
2.双旋转
实际上,针对情形2和3所使用的双旋转只是一种问题的转化方法,其目的还是将无法用单旋转直接解决的情形先转化为单旋转再进一步解决。
针对上图所示的情形2,使用左子树双旋转的解决方法是,先对k1进行一次右子树的单旋转,然后将其转化为1的情形,再通过左子树的单旋转进行平衡的恢复。
实际分析起来双旋转的实现也是简单的,只不过是分别对a节点的儿子和a节点做一次不同方向的单旋转而已。
分析其每次单旋转后的结果我们不难得出:新的根节点将是被新插入的节点。
因为第一次对a节点左子树的单右旋转使得新节点k3变成了a节点的左儿子,那么第二次如同情形1的左单旋转自然会将k3进一步交换到a节点的位置,而a节点将成为k3的右儿子。
左子树双旋转的具体实现代码如下:
AVLTree doubleWithLeftChild(AVLTree k3)
{
k3->left=rotateWithRightChild(k3->left);//先对a节点的左儿子进行右单旋转
return rotateWithLeftChild(k3); //再对a节点进行单左旋转
}
基于单旋转,双旋转的实现是简单的。
AVL树的其他一些操作
- 节点的创建操作
- 平衡化操作
- 插入操作
- 中序遍历
- 层序遍历
下面给出上述完整代码:
#include <stdio.h>
#include <stdlib.h>
typedef struct AVLnode//AVLTree节点定义
{
int data;
struct AVLnode * left;
struct AVLnode * right;
int height;
}AVLnode,* AVLTree;
Height(AVLTree t)
{
return t!=NULL?t->height:-1;
}
AVLTree CreateAVLnode(int data)//创建并返回一个AVLTree的节点
{
AVLTree t=(AVLTree)malloc(sizeof(AVLnode));
t->data=data;
t->height=0;
t->left=NULL;
t->right=NULL;
return t;
}
AVLTree rotateWithLeftChild(AVLTree k2)//左子树单旋转
{
AVLTree k1=k2->left;
k2->left=k1->right;
k1->right=k2;
k1->height=Height(k1->left)>=Height(k1->right)?Height(k1->left)+1:Height(k1->right)+1;
k2->height=Height(k2->left)>=Height(k2->right)?Height(k2->left)+1:Height(k2->right)+1;
return k1;
}
AVLTree rotateWithRightChild(AVLTree k2) //右子树单旋转
{
AVLTree k1=k2->right;
k2->right=k1->left;
k1->left=k2;
k1->height=Height(k1->left)>=Height(k1->right)?Height(k1->left)+1:Height(k1->right)+1;
k2->height=Height(k2->left)>=Height(k2->right)?Height(k2->left)+1:Height(k2->right)+1;
return k1;
}
AVLTree doubleWithLeftChild(AVLTree k3)//左子树双旋转
{
k3->left=rotateWithRightChild(k3->left);
return rotateWithLeftChild(k3);
}
AVLTree doubleWithRightChild(AVLTree k3)//右子树双旋转
{
k3->right=rotateWithLeftChild(k3->right);
return rotateWithRightChild(k3);
}
AVLTree Blance(AVLTree t)//将AVL树进行平衡化操作
{
if(t==NULL) return t;
if(Height(t->left)-Height(t->right)>1)//左子树高度过大,对左子树进行旋转
{
//进一步判断是单旋转还是双旋转
if(Height(t->left->left)>=Height(t->left->right))//如果左边长,那么属于情形1,进行单旋转
{
t=rotateWithLeftChild(t);//左子树单旋转
}
else if(Height(t->left->left)<Height(t->left->right)) //如果右边长,那么属于情形3,进行双旋转
{
t=doubleWithLeftChild(t);//左子树双旋转
}
}
else if(Height(t->right)-Height(t->left)>1)//右子树高度过大,对右子树进行旋转
{
//进一步判断是单旋转还是双旋转
if(Height(t->right->right)>=Height(t->right->left))//情形4,单旋转
{
t=rotateWithRightChild(t);//右子树单旋转
}
else if(Height(t->right->right)<Height(t->right->left))
{
t=doubleWithRightChild(t);
}
}
t->height=Height(t->left)>=Height(t->right)?Height(t->left)+1:Height(t->right)+1;
return t;
}
AVLTree Insert(int data,AVLTree t)//向AVL树中插入新的节点
{
if(t==NULL)
{
return CreateAVLnode(data);
}
else if(data>t->data)
{
t->right=Insert(data,t->right);
}
else if(data<t->data)
{
t->left=Insert(data,t->left);
}
else ;
return Blance(t);//返回平衡后的节点
}
void InOrder(AVLTree t)//中序遍历平衡二叉树
{
if(t!=NULL)
{
InOrder(t->left);
printf("%d ",t->data);
InOrder(t->right);
}
}
main()
{
AVLTree t=NULL;
t=Insert(5,t);
t=Insert(3,t);
t=Insert(6,t);
t=Insert(1,t);
t=Insert(7,t);
t=Insert(8,t);
InOrder(t);
}
这里中序遍历AVL树就像中序遍历二叉排序是一样,可以得到一个从小到大排列的有序数列,但是这无法看出AVL树到底是否处于平衡状态,所以我们决定使用层序遍历来更加直观地观察平衡的结果。
下面简单地描述层序遍历二叉树的算法:
层序遍历二叉树使用队列来实现是简单的,其算法为,先将根节点入队,此时队列不为空,然后以队列不为空为保持循环的条件来进行以下循环操作:
将队首元素出队并打印队首元素,然后如果其左儿子不为空将其左儿子入队,如果其右儿子不为空再将其右儿子入队。
队列数据结构的定义和操作不再给出,直接给出层序遍历二叉树的代码:
void LevelOrder(AVLTree t)//层序遍历二叉树
{
queue q;
AVLTree p,l,r;
Init_queue(&q);
if(t!=NULL)
{
Enqueue(&q,t);
}
else return;
while(q.size!=0)
{
Dequeue(&q,p);
printf("%d ",p->data);
l=p->left;
if(l!=NULL) Enqueue(&q,l);
r=p->right;
if(r!=NULL) Enqueue(&q,r);
}
}
上面创建的AVL树的层序遍历结果为:5 3 7 1 6 8
由此结果可以看出确实该AVLTree确实是平衡的。