9.5 平衡二叉树(AVL)
考虑使用序列{1,2,3,4,5}构建二叉查找树。
显然这棵二叉查找树是链式的。那么,一旦需要对有10^5级别个递增元素的序列构建二叉查找树,也将会得到一棵长长链条式的树,此时对这棵树中结点进行查找的复杂度就会达到0(N),起不到使用二叉查找树来进行数据查询优化的目的。于是需要对树的结构进行调整,使树的高度在每次插入元素后仍然能保持O(logn)的级别,这样能让查询操作仍然是O(logn)的时间复杂度,于是就产生了平衡二叉树。
AVL树仍然是一棵二叉查找树,只是在其基础上增加了“平衡”的要求。所谓平衡是指,对AVL树的任意结点来说,其左子树与右子树的高度之差的绝对值不超过1,其中左子树与右子树的高度之差称为该结点的平衡因子。只要能随时保证每个结点平衡因子的绝对值不超过1,AVL的高度就始终能保持O(logn)
由于需要对每个结点都得到平衡因子,因此需要在树的结构中加入一个变量height,用来记录以当前结点为根结点的子树的高度:
struct Node{
int data ;
int height ;
Node* lchild ;
Node* rchild ;
};
在这种定义下,如果需要新建一个结点,就可以采用如下写法:
//生成一个新节点
Node* newNode( int data ){
Node* node = new Node ;
node->data = data ;
node->height = 1 ; //结点高度初始为1
node->lchild = node->rchild = NULL ;
return node ;
}
显然,可以通过下面函数获取结点root所在子树的高度
//计算root为根结点的子树当前height
int getHeight( Node* root ){
if( root == NULL ){
return 0 ; //空结点高度为0
}
return root->height ;
}
计算平衡因子
//计算平衡因子
int getBalanceFactor(Node* root){
//左子树高度减去右子树高度
return getHeight( root->lchild ) - getHeight( root->rchild ) ;
}
结点root所在子树的height等于其左子树的height与右子树的height的较大值加1.
void updateHeight( Node* root ){
root->height = max( getHeight(root->lchild) , getHeight(root->rchild) )+1 ;
}
9.5.2 平衡二叉树的基本操作
1. 查找操作
和二叉查找树完全相同
//查找
void search( Node* root , int x ){
if( root == NULL ){
return ;
}
if( root->data == x ){
cout << root->data << endl ;
}
else if( root->data > x ){
search( root->lchild , x ) ;
}
else{
search( root->rchild , x ) ;
}
}
2. 插入操作
这个调整过程被称为左旋(Left Rotation),假设指针root指向结点A,指针temp指向结点B,调整步骤如下:
- 让B的左子树成为A的右子树。
- 让A成为B的左子树。
- 将根结点设定为B
对应的代码为
//左旋
void L( Node* &root ){
Node* temp = root->rchild ;
root->rchild = temp->lchild ; //步骤1
temp->lchild = root ; //步骤2
updateHeight(root) ; //更新结点A的高度
updateHeight(temp) ; //更新结点B的高度
root = temp ; //步骤3
}
右旋同理
对应代码为
//右旋
void R( Node* &root ){
Node* temp = root->lchild ;
root->lchild = temp->rchild ;
temp->rchild = root ;
updateHeight(root) ;
updateHeight(temp) ;
root = temp ;
}
关于旋转的讨论到此为止,接下来开始讨论AVL树的插入操作。
假设现在已有一棵平衡二叉树,那么可以预见到,在往其中插入一个结点时,一定会有结点的平衡因子发生变化,此时可能会有结点的平衡因子的绝对值大于1,这样以该结点为根结点的子树就是失衡的,需要进行调整。显然,只有在从根结点到该插入结点的路径上的结点才可能发生平衡因子变化,因此只需对这条路径上失衡的结点进行调整。可以证明,只要把最靠近插入结点的失衡结点调整到正常,路径上的所有结点就都会平衡。
假设最靠近插入结点的失衡结点是A,显然它的平衡因子只可能是2或者-2,很容易发现这两种情况完全对称,因此主要讨论结点A的平衡因子是2的情形。
现在考虑怎样调整这两种树型,才能使树平衡。
先考虑LL型,可以把以C为根结点的子树看作一个整体,然后以结点A作为root进行右旋,便可以达到平衡
LR型调整方法
同样,也有RR型与RL型
对RR型来说,可以把以C为根结点的子树看作一个整体,然后以结点A作为root进行左旋,便可以达到平衡
RL型
至此,对LL型、LR型、RR型、RL型的调整方法都已经讨论清楚,下面做个小小的汇总
首先,AVL树的插入代码是在二叉查找树的插入代码的基础上增加平衡操作的,因此,如果不考虑平衡操作,代码是下面这样的:
//插入
void insert( Node* &root , int x ){
if( root == NULL ){
root = newNode(x) ;
return ;
}
if( root->data > x ){
insert( root->lchild , x ) ;
}
else{
insert( root->rchild , x ) ;
}
}
在这个基础上,由于需要从插入的结点开始从下往上判断结点是否失衡,因此需要在每个insert函数之后更新当前子树的高度,并在这之后根据树型是LL型、LR型、RR型、RL型之一来进行平衡操作,代码如下:
//插入
void insert( Node* &root , int x ){
if( root == NULL ){
root = newNode(x) ;
return ;
}
if( root->data > x ){
insert( root->lchild , x ) ;
updateHeight( root ) ;
if( getBalanceFactor(root) == 2 ){
if( getBalanceFactor(root->lchild) == 1 ){ //LL型
R(root) ;
}
else if( getBalanceFactor(root->lchild) == -1 ){ //LR型
L(root->lchild) ;
R(root) ;
}
}
}
else{
insert( root->rchild , x ) ;
updateHeight( root ) ;
if( getBalanceFactor( root ) == -2 ){
if( getBalanceFactor( root->rchild ) == -1 ){ //RR型
L(root) ;
}
else if( getBalanceFactor( root->rchild ) == 1 ){ //RL型
R(root->rchild);
L(root) ;
}
}
}
}
- LL型 调整方法为 R(root) ;
- LR型 调整方法为 L(root->lchild) ; R(root) ;
- RR型 调整方法为 L(root) ;
- RL型 调整方法为 R(root->rchild) ; L(root) ;
3. AVL的建立
Node* create( int data[] , int n ){
Node* root = NULL ;
for( int i = 0 ; i < n ; i++ ){
insert( root , data[i] ) ;
}
return root ;
}