文章目录
- AVL树的概念
- 平衡化旋转
- 右单旋转
- 左单旋转
- 先左后右双旋转
- 先右后左双旋转
- AVL树的插入
- 根据BST树规则进行节点插入
- 平衡化处理
- 重新连接节点
- 完整的插入函数代码
- AVL树的验证
- AVL树的性能
AVL树的概念
二叉搜索树虽然可以提高查找的效率,但是二叉搜索树有其自身的缺陷,也许是因为输入值不够随机,也许是因为经过某些插入或删除操作,二叉搜索树可能会失去平衡,造成搜索效率低的情况。
树形的平衡与否,并没有一个绝对的测量标准。平衡的意义大概就是:没有任何一个节点过深
。不同的平衡条件,造成了树搜索的不同的效率表现,以及不同的实现复杂度。
AVL树是一个加上了额外平衡条件的二叉搜索树,其具有以下性质:它的左子树和右子树都是AVL树,且左子树和右子树的高度之差的绝对值不超过1。在AVL树中,我们用
平衡因子
来描述每个节点是否平衡。平衡因子用节点的右子树的高度减去左子树的高度所得到的高度差,根据AVL树的定义,任一节点的平衡因子只能取-1,0和1。如果一个节点的平衡因子的绝对值大于1,那么该二叉搜索树就失去了平衡,不再是AVL树了。
假设AVL树有n个节点,其高度可保持在O(lon_2n),平均搜索长度也可保持在O(lon_2n)。
平衡化旋转
AVL树要保证每一个节点的平衡因子不超过1
,当我们往一棵AVL树中插入一个新节点时,就可能会造成原本平衡的AVL树不平衡。此时必须要调整树的结构,让树重新平衡。
平衡化旋转有两类:单旋转和双旋转
。单旋转又分为两类:左单旋转和右单旋转;双旋转也分为两类:先左后右双旋转和先右后左双旋转。
左单旋转和右单旋转是镜像的。
右单旋转
旋转过程如图所示:
代码如下:
template <class Type>
void AVL<Type>::RotateR(AVLNode<Type> *&ptr)
{
AVLNode<Type>* subR = ptr;
ptr = subR->leftchild;
subR->leftchild = ptr->rightchild;
ptr->rightchild = subR;
ptr->bf = subR->bf = 0;
}
左单旋转
左单旋转是右单旋转的镜像。
代码如下:
template <class Type>
void AVL<Type>::RotateL(AVLNode<Type> *&ptr)
{
AVLNode<Type>* subL = ptr;
ptr = subL->rightchild;
subL->rightchild = ptr->leftchild;
ptr->leftchild = subL;
ptr->bf = subL->bf = 0;
}
先左后右双旋转
双旋转总是考虑三个节点,一个节点是调整后的根节点,一个是未来的左子树节点,另一个是未来的右子树节点。我们先左后右双旋转也就是把未来的左子树节点进行一次左单旋转调整到应当的位置,再把未来的右子树节点进行一次右单旋转调整到应当的位置。
我们把先左后右双旋转分为先一次的左单旋转和再一次的右单旋转
,下面我们来看一下先左后右双旋转发生的情况。
情形一:
情形二:
上述两种情况都是先左后右双旋转的情况,不同的是插入节点位置的不同,插入位置主要影响的是平衡因子,那我们用什么来区分两种情况呢?主要是
插入节点的父节点的平衡因子的正负
,如果是大于0,那新节点就是在右子树的位置插入的,如果是小于0,那就是在左子树的位置插入的,概括两种情况,我们用下面的代码来描述先左后右双旋转:
template <class Type>
void AVL<Type>::RotateLR(AVLNode<Type> *&ptr)
{
AVLNode<Type>* subR = ptr;
AVLNode<Type>* subL = ptr->leftchild;
ptr = subL->rightchild;
subL->rightchild = ptr->leftchild;//ptr成为新根前甩掉它左边的负载
ptr->leftchild = subL;//左单旋转,ptr成为新根
//调整subL的bf
if (ptr->bf <= 0)
subL->bf = 0;
else
subL->bf = -1;
subR->leftchild = ptr->rightchild;//ptr成为新根前甩掉它右边的负载
ptr->rightchild = subR;//右单旋转,ptr成为新根
//调整subR的bf
if (ptr->bf >= 0)
subR->bf = 0;
else
subR->bf = 1;
ptr->bf = 0;
}
先右后左双旋转
先右后左双旋转是先左后右双旋转的镜像。我们也是将双旋转拆分成两个单旋转,先是右单旋转,再是左单旋转,根据插入位置的不同,我们在此讨论这两种情形。
情形一:
情形二:
同样的,新插入节点的左右位置不同会导致调整平衡后的各节点平衡因子不一样,概括上述两种情况,我们的先右后左双旋转代码如下:
void AVL<Type>::RotateRL(AVLNode<Type> *&ptr)
{
AVLNode<Type>* subL = ptr;
AVLNode<Type>* subR = ptr->rightchild;
ptr = subR->leftchild;
subR->leftchild = ptr->rightchild;//ptr成为新根前甩掉它右边的负载
ptr->rightchild = subR;//右单旋转,ptr成为新根
if (ptr->bf >= 0)
subR->bf = 0;
else
subR->bf = 1;
subL->rightchild = ptr->leftchild;//ptr成为新根前甩掉它左边的负载
ptr->leftchild = subL;//左单旋转,ptr成为新根
if (ptr->bf <= 0)
subL->bf = 0;
else
subL->bf = -1;
ptr->bf = 0;
}
AVL树的插入
当向一棵本来是高度平衡的AVL树中插入新节点时,如果树中某个节点的平衡因子的绝对值大于1了,那就出现了不平衡,就要做平衡化处理。
首先需要知道的是,出现不平衡的节点一定是插入新节点所经过路径上面的节点,我们在插入节点后,要去检查树是否平衡,其实检查的也就是插入新节点所经过路径上面的节点是否平衡,所以我们需要记录下来新节点插入过程中所走的路径,那用什么记录呢?考虑到后经过的节点先检验是否平衡,我们自然想到用栈
结构。
我们插入新节点大致经过三个步骤:根据BST树插入节点规则进行插入、平衡化处理、重新连接节点。
根据BST树规则进行节点插入
下面这段代码是在二叉搜索树的插入代码上增加了栈记录插入路径的代码:
AVLNode<Type> *p = t, *pr = nullptr;
stack<AVLNode<Type>*> st;
while(p != nullptr)
{
if(v == p->data)
return false;
pr = p;
st.push(pr);
if(v < p->data)
p = p->leftChild;
else
p = p->rightChild;
}
p = new AVLNode<Type>(v);
if(pr == nullptr)
{
t = p;
return true;
}
if(p->data < pr->data)
pr->leftChild = p;
else
pr->rightChild = p;
平衡化处理
设新插入的节点为p,从节点p到根节点的路径上,每节点为根的子树的高度都可能会改变,因此,在每执行一次二叉搜索树的插入运算后,我们都需要从新插入的p节点开始,沿该节点的插入路径向根节点方向
回溯
,修改各节点的平衡因子,调整子树的高度,恢复被破坏的平衡性质
。我们前面说了,我们用栈结构来记录插入路径。
新节点p的平衡因子为0,这是毫无疑问的,现在要来看它的父节点pr,如果p是pr的左子树,那么pr的平衡因子增加1,否则减少1.
修改平衡因子的代码如下:
pr = st.top();
st.pop();
if (p == pr->leftchild)
pr->bf--;
else
pr->bf++;
修改过后,pr的平衡因子有三种情况:
- 1、pr的平衡因子为0. 说明p是在pr较矮的子树上插入的,在p插入后,节点pr依旧平衡,整棵树的高度并没有发生增减,此时从pr到根的路径上个以各节点为根的子树的高度不变,从而各节点的平衡因子不变,不需要继续调整平衡,可以退出对栈中元素平衡因子以及是否平衡的处理,退出循环,返回主程序。
代码如下:
if (pr->bf == 0)
break;
- 2、pr的平衡因子的绝对值为1. 说明插入前pr的平衡因子是0,插入新节点p后,pr这颗子树不需要进行平衡化旋转,但整棵树的高度增加,需要从pr向根的方向回溯,考察栈中其他节点的平衡状态。
代码如下:
if (pr->bf == 1 || pr->bf == -1)
p = pr;//回溯
- 3、节点pr的平衡因子的绝对值为2,说明新节点在较高的子树上进行插入,造成了不平衡,需要进行平衡化旋转,此时我们根据p和pr节点的平衡因子的
正负情况
选择哪种平衡化旋转。
在pr节点的平衡因子小于0的情况下,p的平衡因子也小于0,那就进行先左后右双旋转,否则进行左单旋转,不存在pr节点的平衡因子小于0,p的平衡因子等于0的情况,因为这种情况是在p是pr的左子树,且p有右子树,又在p的左子树上进行插入产生的。很明显,这种情况下,在p的左子树进行插入之前树就已经不平衡了,已经需要调整平衡了。
在pr节点的平衡因子大于0的情况下,如果p节点的平衡因子也大于0,那就进行右单旋转,否则就进行先右后左双旋转。
代码如下:
if (pr->bf < 0)
{
if (p->bf < 0) // /
RotateR(pr);
else // <
RotateLR(pr);
}
else
{
if (p->bf > 0) // "\"
RotateL(pr);
else // >
RotateRL(pr);
}
重新连接节点
需要单独考虑的一种情况就是,树本身是一棵空树
,我们插入的节点是树根
。
代码如下:
if(st.empty())
t = pr;
else
{
AVLNode<Type> *ppr = st.top();
if(pr->data < ppr->data)
ppr->leftChild = pr;
else
ppr->rightChild = pr;
}
完整的插入函数代码
在这里插入代码片template <class Type>
class AVL;
//定义AVL树的节点类型
template <class Type>
class AVLNode
{
friend class AVL<Type>;
public:
AVLNode(Type d = Type(), AVLNode<Type>*left = nullptr, AVLNode<Type>*right = nullptr)
:data(d), leftchild(left), rightchild(right), bf(0)
{}
~AVLNode()
{}
private:
AVLNode<Type>* leftchild;
AVLNode<Type>* rightchild;
Type data;
int bf;//平衡因子
};
//定义AVL树
template <class Type>
class AVL
{
public:
AVL():root(nullptr)
{}
~AVL()
{
Destroy(root);
}
public:
bool Insert(const Type &key)
{
return Insert(root, key);
}
bool Remove(const Type &key)
{
return Remove(root, key);
}
protected:
bool Insert(AVLNode<Type>*&t, const Type& key);
bool Remove(AVLNode<Type>*&t, const Type& key);
protected:
void RotateR(AVLNode<Type> *&ptr);
void RotateL(AVLNode<Type> *&ptr);
void RotateLR(AVLNode<Type> *&ptr);
void RotateRL(AVLNode<Type> *&ptr);
public:
void Destroy(AVLNode<Type>*&t)
{
if (t != nullptr)
{
Destroy(t->leftchild);
Destroy(t->rightchild);
delete t;
t = nullptr;
}
}
private:
AVLNode<Type> *root;
};
template <class Type>
void AVL<Type>::RotateR(AVLNode<Type> *&ptr)
{
AVLNode<Type>* subR = ptr;
ptr = subR->leftchild;
subR->leftchild = ptr->rightchild;
ptr->rightchild = subR;
ptr->bf = subR->bf = 0;
}
template <class Type>
void AVL<Type>::RotateL(AVLNode<Type> *&ptr)
{
AVLNode<Type>* subL = ptr;
ptr = subL->rightchild;
subL->rightchild = ptr->leftchild;
ptr->leftchild = subL;
ptr->bf = subL->bf = 0;
}
template <class Type>
void AVL<Type>::RotateLR(AVLNode<Type> *&ptr)
{
AVLNode<Type>* subR = ptr;
AVLNode<Type>* subL = ptr->leftchild;
ptr = subL->rightchild;
subL->rightchild = ptr->leftchild;//ptr成为新根前甩掉它左边的负载
ptr->leftchild = subL;//左单旋转,ptr成为新根
//调整subL的bf
if (ptr->bf <= 0)
subL->bf = 0;
else
subL->bf = -1;
subR->leftchild = ptr->rightchild;//ptr成为新根前甩掉它右边的负载
ptr->rightchild = subR;//右单旋转,ptr成为新根
//调整subR的bf
if (ptr->bf >= 0)
subR->bf = 0;
else
subR->bf = 1;
ptr->bf = 0;
}
template <class Type>
void AVL<Type>::RotateRL(AVLNode<Type> *&ptr)
{
AVLNode<Type>* subL = ptr;
AVLNode<Type>* subR = ptr->rightchild;
ptr = subR->leftchild;
subR->leftchild = ptr->rightchild;//ptr成为新根前甩掉它右边的负载
ptr->rightchild = subR;//右单旋转,ptr成为新根
if (ptr->bf >= 0)
subR->bf = 0;
else
subR->bf = 1;
subL->rightchild = ptr->leftchild;//ptr成为新根前甩掉它左边的负载
ptr->leftchild = subL;//左单旋转,ptr成为新根
if (ptr->bf <= 0)
subL->bf = 0;
else
subL->bf = -1;
ptr->bf = 0;
}
template <class Type>
bool AVL<Type>::Insert(AVLNode<Type>*&t, const Type& v)
{
AVLNode<Type> *p = t, *pr = nullptr;
stack<AVLNode<Type>*> st;//借助栈结构保存节点插入的路线
//确定插入位置
while (p != nullptr)
{
if (p->data == v)
return false;
pr = p;
st.push(pr);
if (v < p->data)
p = p->leftchild;
else
p = p->rightchild;
}
//新建节点
p = new AVLNode<Type>(v);
if (pr == nullptr)
{
t = p;
return true;
}
if (p->data < pr->data)
pr->leftchild = p;
else
pr->rightchild = p;
//调整平衡
while (!st.empty())
{
pr = st.top();
st.pop();
if (p == pr->leftchild)
pr->bf--;
else
pr->bf++;
if (pr->bf == 0)
break;
if (pr->bf == 1 || pr->bf == -1)
p = pr;//回溯
else
{
//四种需要调整树的结构的情况
if (pr->bf < 0)
{
if (p->bf < 0) // /
RotateR(pr);
else // <
RotateLR(pr);
}
else
{
if (p->bf > 0) // "\"
RotateL(pr);
else // >
RotateRL(pr);
}
break;
}
}
//重新连接
if (st.empty())
t = pr;
else
{
AVLNode<Type>* ppr = st.top();
if (pr->data < ppr->data)
ppr->leftchild = pr;
else
ppr->rightchild = pr;
}
return true;
}
AVL树的验证
AVL树是在二叉搜索树的基础上加入了对平衡性的限制,因此要验证AVL树,可以分为两步:
- 验证树为二叉搜索树
如果中序遍历得到一个有序的序列,那么就说明是二叉搜索树
- 验证树为平衡树,每个节点的子树的高度差的绝对值不超过1,也就是平衡因子的绝对值不超过1,同时要注意节点的平衡因子是否计算正确。
AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树
,要求平衡因子的绝对值不超过1,这样保证了搜索查询的时间复杂度为log2(n),十分高效,但是同时,因为要保证维护树的绝对平衡,在对AVL树做一些结构修改的操作时,性能就十分低下,比如插入某个值,就可能会引起多次旋转,更差的是在删除时,有可能要让旋转持续到根的位置。考虑到AVL树的特性,当我们需要一种查询十分高效且有序
的数据结构,而且数据的个数为静态的
,也就意味着树的结构不会发生变化,此时可以考虑AVL树,如果我们的结构经常改变,经常需要插入或者删除一些数据,AVL树就不是那么合适了。