首先,在讨论AVL树之前,我们得明白一点,那就是为什么得有AVL树?
AVL树与二叉搜索树一样都是用于搜索的二叉树,也可以这么说AVL树是特殊的二叉搜索树,它与二叉搜索树的主要区别在于AVL树中给定了平衡因子(左右子树的高度差),而且明确规定,平衡因子最大不能超过1,最小不能小于-1,因此我们的AVL树中左右子树的高度差只能为0,1,-1,也就是说不会出现一段较长的单支的情况,而对于二叉搜索树而言,单支树的情况完全可能出现。
而对于单支树这种情况,我们对于其的搜索就相当于对一个单链表进行搜索,时间复杂度由原来的O(log2(N))变成我们的O(N),效率大大降低,而与此相对的,AVL树是不会出现一段太长的单支情况(由平衡因子的限制),所以我们的时间复杂度依旧为O(log2(N)),这也就是为什么要引入我们的AVL树。
一.AVL树的概念:
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
1、它的左右子树都是AVL树
2、左子树和右子树高度之差(简称平衡因子)的绝对值不超过1(-1、0、1)
二.AVL树的数据结构
1.节点的定义
template<class K,class V> //这里我们用键值对来描述一个结点
struct Node
{
Node(const K& key,const V& value)
:_pLeft(NULL)
,_pRight(NULL)
,_parent(NULL)
,_key(key)
,_value(value)
,_bf(0)
{}
Node<K,V>* _pLeft;
Node<K,V>* _pRight;
Node<K,V>* _parent;
K _key;
V _value;
int _bf; //平衡因子
};
2.结点的插入
对于结点的插入操作,我们来仔细分析一下:
首先,对于AVL树,它是一个特殊的二叉搜索树,所以它的插入算法与二叉搜索树有很大的共通之处(关于二叉搜索树的插入算法,上一篇博文已经分析过,这里不再赘述),而对于AVL树的插入还需要满足的一点就是当我们将一个结点插入之后,这棵树依旧是我们的AVL树,这一点是绝对不能变的,所以我们可以想一下,在我们插入一个结点的时候,对应的双亲的平衡因子是不是很有可能超过1,或者小于-1,那这样就显然违背了AVL树的性质,所以我们就需要在插入之后做出一些调整,也就是我们的旋转操作。
下面就通过图来分析旋转操作的所有可能情况以及它们对应的算法:
①左单旋:在较高的右子树的右侧插入
这里我们是将我们的子树的根节点的右孩子(也就是40对应的节点)作为旋转之后的根节点,而原来的根节点(也就是20所对应的节点)成为新的根节点(40)的左孩子,而原本(40)这个节点的左孩子成为(20)的右孩子。上述的情况之中,(10),(30)所对应的节点可能为NULL,但是处理方法还是一样的。无非就是可能会出现下面这种情况:
实现算法如下:
void RotateL(Node* parent) //左旋
{
Node* pSubR=parent->_pRight;
Node* pSubRL=pSubR->_pLeft;
Node* pparent=parent->_parent;
pSubR->_pLeft=parent;
parent->_parent=pSubR;
parent->_pRight=pSubRL;
if(pSubRL)
pSubRL->_parent=parent;
pSubR->_parent=pparent;
if(pparent==NULL)
_pRoot=pSubR;
else
{
if(pparent->_pLeft==parent)
pparent->_pLeft=pSubR;
else
pparent->_pRight=pSubR;
}
pSubR->_bf=parent->_bf=0;
}
②右单旋:在较高的左子树的左侧插入
这里与左单旋类似,将原子树的根节点(50)作为旋转之后的根节点的右孩子,而原本根节点的左孩子(30)作为旋转之后的根节点,而它的右孩子(40)则作为(50)的左孩子,同样,这里(40)和(60)对应的节点有可能为NULL,但操作方法还是一样的,而其中的特殊情况无非就是下面这种情况:
实现算法如下:
void RotateR(Node* parent) //右旋
{
Node* pSubL=parent->_pLeft;
Node* pSubLR=pSubL->_pRight;
Node* pparent=parent->_parent;
pSubL->_pRight=parent;
parent->_parent=pSubL;
parent->_pLeft=pSubLR;
if(pSubLR)
pSubLR->_parent=parent;
pSubL->_parent=pparent;
if(pparent==NULL)
_pRoot=pSubL;
else
{
if(pparent->_pLeft==parent)
pparent->_pLeft=pSubL;
else
pparent->_pRight=pSubL;
}
parent->_bf=pSubL->_bf=0;
}
③左右双旋(先左旋,后右旋):在较高的左子树的右侧插入
由于对在较高的左子树的右侧插入节点(25),无法通过简单的左单旋或者是右单旋来直接旋转得到符合AVL树性质的子树,因此我们在这里先进行左单旋得到一个类似于前面提到过的可进行右单旋的子树,再进行右单旋得到我们想要的符合AVL性质的子树。
这里需要注意一种特殊情况:也就是当我们的原来的子树的根节点的右子树为空时
实现算法如下:
void RotateLR(Node* parent)
{
Node* pParent=parent;
Node* pSubL=parent->_pLeft;
int bf=parent->_pLeft->_pRight->_bf;
RotateL(parent->_pLeft);
RotateR(parent);
if(parent->_pRight)
{
if(bf==1)
pSubL->_bf=-1;
else
pParent->_bf=1;
}
}
④右左双旋:在较高的右子树的左侧插入
由于对在较高的右子树的左侧插入节点(30),无法通过简单的左单旋或者是右单旋来直接旋转得到符合AVL树性质的子树,因此我们在这里先进行右单旋得到一个类似于前面提到过的可进行左单旋的子树,再进行左单旋得到我们想要的符合AVL性质的子树。
这里也需要注意一种特殊情况:也就是当我们的原来的子树的根节点的左子树为空时
void RotateRL(Node* parent)
{
Node* pParent=parent;
Node* pSubR=parent->_pRight;
int bf=parent->_pRight->_pLeft->_bf;
RotateR(parent->_pRight);
RotateL(parent);
if(parent->_pLeft)
{
if(bf==-1)
pSubR->_bf=1;
else
pParent->_bf=-1;
}
}
以上四类情况就是我们旋转所对应的情况,当然插入的节点可以放在对应双亲的左或右,这不会影响旋转的算法,这里不做赘述。
插入算法所有代码如下:
bool Insert(const K& key,const V& value)
{
if(_pRoot==NULL)
{
_pRoot=new Node(key,value);
return true;
}
Node* pcur=_pRoot;
Node* parent=NULL;
while(pcur)
{
if(key<pcur->_key)
{
parent=pcur;
pcur=pcur->_pLeft;
}
else if(key>pcur->_key)
{
parent=pcur;
pcur=pcur->_pRight;
}
else
return false;
}
pcur=new Node(key,value);
if(key<parent->_key)
{
parent->_pLeft=pcur;
}
else
{
parent->_pRight=pcur;
}
pcur->_parent=parent;
while(pcur->_parent!=NULL)
{
if(parent->_pLeft==pcur)
parent->_bf--;
else
parent->_bf++;
if(parent->_bf==0) //若双亲平衡因子为0,则说明在插入这个结点之后,并不会对其所在子树高度产生影响,不会影响平衡
return true;
else if(abs(parent->_bf)==2) //若双亲的平衡因子为2或-2,则在此处不满足AVL树的条件,需要进行旋转调整
{ //注意:由于调整之后,子树的高度与插入之前相等,由此在这里不需要向上调整,
//已经平衡(插入之前这棵树必然满足AVL树的条件),可以直接跳出
if(parent->_bf==2&&pcur->_bf==1) //左单旋
RotateL(parent);
else if(parent->_bf==-2&&pcur->_bf==-1) //右单旋
RotateR(parent);
else if(parent->_bf==-2&&pcur->_bf==1) //先左旋后右旋
{
RotateLR(parent);
}
else //先右旋后左旋
{
RotateRL(parent);
}
break;
}
else //若双亲平衡因子为1或-1,则说明在插入该结点之后,对其所在子树高度产生了影响,但在此处依旧满足AVL树条件,所以需要继续向上调整
{
pcur=parent;
parent=pcur->_parent;
}
}
return true;
}
3.结点的删除
结点的删除其实与二叉搜索树的删除算法大致相同,只不过与插入算法一样,在平衡因子超过1或-1,也就是为2或-2时,需要进行调整,也就是进行旋转操作调整,这与插入的旋转调整几乎一样,算法大致相同:
bool Delete(const K& key)
{
if(_pRoot==NULL)
return false;
Node* pcur=_pRoot;
Node* parent=NULL;
while(pcur)
{
if(key<pcur->_key)
{
parent=pcur;
pcur=pcur->_pLeft;
}
else if(key>pcur->_key)
{
parent=pcur;
pcur=pcur->_pRight;
}
else
{
if(pcur->_pLeft==NULL)
{
if(parent==NULL)
{
_pRoot=pcur->_pRight;
}
else if(pcur==parent->_pLeft)
{
parent->_pLeft=pcur->_pRight;
}
else
{
parent->_pRight=pcur->_pRight;
}
if(pcur->_pRight)
pcur->_pRight->_parent=parent;
}
else if(pcur->_pRight==NULL)
{
if(parent==NULL)
_pRoot=pcur->_pLeft;
else if(pcur==parent->_pLeft)
parent->_pLeft=pcur->_pLeft;
else
parent->_pRight=pcur->_pLeft;
if(pcur->_pLeft)
pcur->_pLeft->_parent=parent;
}
else
{
Node* pDel=pcur->_pRight;
parent=pcur;
while(pDel->_pLeft)
{
parent=pDel;
pDel=pDel->_pLeft;
}
pcur->_key=pDel->_key;
pcur->_value=pDel->_value;
if(pDel==parent->_pLeft)
parent->_pLeft=pDel->_pRight;
else
parent->_pRight=pDel->_pRight;
if(pDel->_pRight)
pDel->_pRight->_parent=parent;
pcur=pDel;
}
Node* pNode=pcur;
while(parent) //从被删除结点的双亲开始向上更新平衡因子
{
if(pNode->_key<parent->_key)
parent->_bf++;
else
parent->_bf--;
if(parent->_bf==1||parent->_bf==-1) //若双亲平衡因子为1或-1,则说明删除的结点对其所在子树的高度并没有影响,因此这里依旧满足AVL树的条件
break;
else if(parent->_bf==0) //若双亲平衡因子为0,则说明对其所在子树高度产生影响,但在此处依旧满足AVL树条件,所以得向上继续更新
{
pNode=parent;
parent=parent->_parent;
}
else //若双亲平衡因子为2或-2,则在此处不满足AVL树的条件,需要进行一定的旋转调整
{ //注意:此处在进行过调整之后,所在子树的高度必然-1,由此并不能保证是否向上还需要调整,由此这里千万不能像插入算法一样直接跳出
Node* pParent=parent->_parent;
if(parent->_bf==-2)
{
if(parent->_pLeft->_bf==-1) //
{
pNode=parent->_pLeft;
RotateR(parent);
}
else
{
pNode=parent->_pLeft->_pRight;
RotateLR(parent);
}
}
else
{
if(parent->_pRight->_bf==1)
{
pNode=parent->_pRight;
RotateL(parent);
}
else
{
pNode=parent->_pRight->_pLeft;
RotateRL(parent);
}
}
parent=pParent;
}
}
delete pcur;
return true;
}
}
return false;
}
266

被折叠的 条评论
为什么被折叠?



