一、为什么要有平衡二叉树
二叉搜索树一定程度上可以提高搜索效率,但是当原序列有序时,例如序列 A = {1,2,3,4,5,6},构造二叉搜索树如图 1.1。依据此序列构造的二叉搜索树为右斜树,同时二叉树退化成单链表,搜索效率降低为 O(n)。
在此二叉搜索树中查找元素 6 需要查找 6 次。
二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。同样的序列 A,将其改为图 1.2 的方式存储,查找元素 6 时只需比较 3 次,查找效率提升一倍。
可以看出当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高。
这种左右子树的高度相差不超过 1 的树为平衡二叉树。
二、平衡二叉树的定义
typedef struct AVLNode *Tree;
typedef int ElementType;
class AVLNode{
public:
int depth; //深度,这里计算每个结点的深度,通过深度的比较可得出是否平衡
AVLNode* parent; //该结点的父节点
ElementType val; //结点值
AVLNode* lchild;
AVLNode* rchild;
AVLNode(int val=0) {
this->parent = NULL;
this->depth = 0;
this->lchild = NULL;
this->rchild = NULL;
this->val=val;
}
};
平衡二叉查找树:简称平衡二叉树。由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。它具有如下几个性质:
(1)可以是空树。
(2)假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。平衡之意,如天平,即两边的分量大约相同。
例如图 2.1 不是平衡二叉树,因为结点 60 的左子树不是平衡二叉树。
三、平衡因子
定义:某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。
四、AVL树插入时的失衡与调整
节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2,树失去平衡。以节点 66 为父节点的那颗树就称为 最小失衡子树。
最小失衡子树:在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1 的结点为根的子树称为最小不平衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。
平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现的。根据旋转的方向有两种处理方式,左旋 与 右旋 。
旋转的目的就是减少高度,通过降低整棵树的高度来平衡。哪边的树高,就把那边的树向上旋转。
4.1 左旋
上图为例子加入新节点 99 后, 节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2。为保证树的平衡,此时需要对节点 66 做出旋转,因为右子树高度高于左子树,对节点进行左旋操作,流程如下:
(1)节点的右孩子替代此节点位置 (2)右孩子的左子树变为该节点的右子树 (3)节点本身变为右孩子的左子树
1、节点的右孩子替代此节点位置 —— 节点 66 的右孩子是节点 77 ,将节点 77 代替节点 66 的位置
2、右孩子的左子树变为该节点的右子树 —— 节点 77 的左子树为节点 75,将节点 75 挪到节点 66 的右子树位置
3、节点本身变为右孩子的左子树 —— 节点 66 变为了节点 77 的左子树
4.2 右旋
右旋操作与左旋类似,操作流程为:
(1)节点的左孩子代表此节点
(2)节点的左孩子的右子树变为节点的左子树
(3)将此节点作为左孩子节点的右子树。
(该操作中的数值为75节点有误,应该是65才是对的)
五、AVL树的四种插入节点方式
假设一颗 AVL 树的某个节点为 A,有四种操作会使 A 的左右子树高度差大于 1,从而破坏了原有 AVL 树的平衡性。平衡二叉树插入节点的情况分为以下四种:
判断是哪一种情况的方式:
如果存在失衡,先找离插入点最近的失衡节点,然后看插入的节点是在失衡节点的左边还是右边,如果在左边就属于L,反之就是R,然后再看插入节点的父亲节点(未插入新的节点之前为叶节点,如果插入后是失衡的)是属于左叶子还是有叶子结点,如果是左叶子节点则是L,右叶子节点为R。
插入方式 | 描述 | 旋转方式 |
---|---|---|
LL | 在 A 的左子树根节点的左子树上插入节点而破坏平衡 | 右旋转 |
RR | 在 A 的右子树根节点的右子树上插入节点而破坏平衡 | 左旋转 |
LR | 在A的左子树根节点的右子树上插入节点而破坏平衡 | 先左旋后右旋 |
RL | 在 A 的右子树根节点的左子树上插入节点而破坏平衡 | 先右旋后左旋 |
无论是哪一种情况,首先找到离插入点最近的失衡节点,再针对该节点进行分析,是属于哪一种情况的插入方式。
5.1 A的左孩子的左子树插入节点 LL类型
//LL型调整函数
//返回:新父节点
Tree LL_rotate(Tree node){
//node为离操作结点最近的失衡的结点
Tree parent=NULL,son;
//获取失衡结点的父节点
parent=node->parent;
//获取失衡结点的左孩子
son=node->lchild;
//设置son结点右孩子的父指针
if (son->rchild!=NULL) son->rchild->parent=node;
//失衡结点的左孩子变更为son的右孩子
node->lchild=son->rchild;
//更新失衡结点的高度信息
update_depth(node);
//失衡结点变成son的右孩子
son->rchild=node;
//设置son的父结点为原失衡结点的父结点
son->parent=parent;
//如果失衡结点不是根结点,则开始更新父节点
if (parent!=NULL){
//如果父节点的左孩子是失衡结点,指向现在更新后的新孩子son
if (parent->lchild==node){
parent->lchild=son;
}else{
//父节点的右孩子是失衡结点
parent->rchild=son;
}
}
//设置失衡结点的父亲
node->parent=son;
//更新son结点的高度信息
update_depth(son);
return son;
}
5.2 A的右孩子的右子树插入节点RR类型
//RR型调整函数
//返回新父节点
Tree RR_rotate(Tree node){
//node为离操作结点最近的失衡的结点
Tree parent=NULL,son;
//获取失衡结点的父节点
parent=node->parent;
//获取失衡结点的右孩子
son=node->rchild;
//设置son结点左孩子的父指针
if (son->lchild!=NULL){
son->lchild->parent=node;
}
//失衡结点的右孩子变更为son的左孩子
node->rchild=son->lchild;
//更新失衡结点的高度信息
update_depth(node);
//失衡结点变成son的左孩子
son->lchild=node;
//设置son的父结点为原失衡结点的父结点
son->parent=parent;
//如果失衡结点不是根结点,则开始更新父节点
if (parent!=NULL){
//如果父节点的左孩子是失衡结点,指向现在更新后的新孩子son
if (parent->lchild==node){
parent->lchild=son;
}else{
//父节点的右孩子是失衡结点
parent->rchild=son;
}
}
//设置失衡结点的父亲
node->parent=son;
//更新son结点的高度信息
update_depth(son);
return son;
}
5.3 A的左孩子的右子树插入节点(LR类型)
若 A 的左孩子节点 B 的右子树 E 插入节点 F ,导致节点 A 失衡,如图:
A 的平衡因子为 2 ,若仍按照右旋调整,则变化后的图形为这样:
经过右旋调整发现,调整后树仍然失衡,说明这种情况单纯的进行右旋操作不能使树重新平衡。那么这种插入方式需要执行两步操作,使得旋转之后为 原来根结点的左孩子的右孩子作为新的根节点 。
(1)对失衡节点 A 的左孩子 B 进行左旋操作,即上述 RR 情形操作。 (2)对失衡节点 A 做右旋操作,即上述 LL 情形操作。
调整过程如下:
也就是说,经过这两步操作,使得 原来根节点的左孩子的右孩子 E 节点成为了新的根节点。
//LR型,先左旋转,再右旋转
//返回:新父节点
Tree LR_rotate(Tree node){
RR_rotate(node->lchild);
return LL_rotate(node);
}
5.4 A的右孩子的左子树插入节点(RL类型)
右孩子插入左节点的过程与左孩子插入右节点过程类似,也是需要执行两步操作,使得旋转之后为 原来根结点的右孩子的左孩子作为新的根节点 。
(1)对失衡节点 A 的右孩子 C 进行右旋操作,即上述 LL 情形操作。 (2)对失衡节点 A 做左旋操作,即上述 RR 情形操作。
//RL型,先右旋转,再左旋转
//返回:新父节点
Tree RL_rotate(Tree node){
LL_rotate(node->rchild);
return RR_rotate(node);
}
5.5 辅助代码
//更新当前深度
void update_depth(Tree node){
if (node==NULL){
return;
}else{
int depth_Lchild=get_balance(node->lchild); //左孩子深度
int depth_Rchild=get_balance(node->rchild); //右孩子深度
node->depth=max(depth_Lchild,depth_Rchild)+1;
}
}
//获取当前结点的深度
int get_balance(Tree node){
if (node==NULL){
return 0;
}
return node->depth;
}
//返回当前平衡因子
int is_balance(Tree node){
if (node==NULL){
return 0;
}else{
return get_balance(node->lchild)-get_balance(node->rchild);
}
}
六、AVL树删除节点
(1)删除叶子节点 (2)删除的节点只有左子树 (3)删除的节点只有右子树 (4)删除的节点既有左子树又有右子树
只不过 AVL 树在删除节点后需要重新检查平衡性并修正,同时,删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对插入栈中的弹出的第一个非平衡节点进行修正,而删除操作需要修正栈中的所有非平衡节点。
删除操作的大致步骤如下:
(1)以前三种情况为基础尝试删除节点,并将访问节点入栈。
(2)如果尝试删除成功,则依次检查栈顶节点的平衡状态,遇到非平衡节点,即进行旋转平衡,直到栈空。
(3)如果尝试删除失败,证明是第四种情况。这时先找到被删除节点的右子树最小节点并删除它,将访问节点继续入栈。(找左子树最大的节点,找右子树最小的节点,确保可以要删除的节点交换后是不会影响查找二叉树的性质的)
(4)再依次检查栈顶节点的平衡状态和修正直到栈空。
对于删除操作造成的非平衡状态的修正,可以这样理解:对左或者右子树的删除操作相当于对右或者左子树的插入操作,然后再对应上插入的四种情况选择相应的旋转就好了。
利用栈的特性来保存遍历平衡二叉树的节点,其实在插入的时候也可以这么操作,在AVL Tree中插入数据的时候,可以根据二叉查找树的特性来找到要插入的叶子位置,在寻找对应位置的时候将遍历到的节点一个个都进栈,插入完了之后,一个个出栈来更新其深度,顺便判断到离插入点最近的失衡节点是哪一个。