数据结构与算法 -- 平衡二叉树的构建
前言
上一篇学习了一些常见的静态查找和动态查找中二叉搜索树的查找,插入和删除操作。在构建一个二叉搜索树的时候,假如给定的数据是一直递增的,那么就会一直存储在右子树上,构成一个斜树。这时在对其做查找时,效率一样很低。
那么在构建二叉搜索树的时候,怎么解决这样问题呢?接下来我们介绍一下利用平衡二叉树解决二叉搜索树失衡的问题。
1. 平衡二叉树构建分析
平衡二叉树:是一种二叉排序树,其中每一个结点的左右子树的高度差至多等于1。平衡二叉树又称为AVL树。
高度平衡:要么是一个空树,要么左右子树都是平衡二叉树。且左子树和右子树的深度差绝对值不超过1;我们将二叉树上左子树的深度减去右子树的深度的值称为平衡因子BF
最小不平衡子树:距离插入点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树。
如上图中,当插入新节点37
时,就会打乱原有的平衡,图中圈出来的就是最小不平衡子树。
平衡二叉树构建的基本思想:在构建二叉排序树的过程中,每当插入新结点是,先检查是否因插入而破坏了树的平衡性,若是,则找到最小不平衡子树,在保证二叉排序树特性的前提下,调整最小不平衡子树中各节点之间的链接关系,进行左旋或者右旋,使之成为最新的平衡子树。
接下来,我们举个例子分析一下:
假设数组a[10] = {3,2,1,4,5,6,7,10,9,8}
。
在构建正常的二叉排序树,如下:
显然是失衡的,不能构成平衡二叉树。
接下来,我们来模拟一下平衡二叉树:
- 首先插入
结点 3,2,1
的过程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LMz4zGHC-1590137686477)(https://user-gold-cdn.xitu.io/2020/5/22/1723a3da69fa1dbc?w=761&h=356&f=png&s=22368)]
当依次插入结点3、2、1
后,结点3
的平衡因子是2
,进行右旋。
- 插入
结点 4
插入结点4
后,没有失衡
- 插入
结点 5
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0au4hsI3-1590137686492)(https://user-gold-cdn.xitu.io/2020/5/22/1723a53e8c4c4296?w=867&h=408&f=png&s=28139)]
当插入结点5
时,结点3
的平衡因子为-2
,成为最小不平衡子树,进行左旋。
- 插入
结点 6
当插入结点6
时,结点2
的平衡因子为-2
,成为最小不平衡子树,进行左旋。
- 插入
结点7
结点5
的平衡因子为-2
,成为最小不平衡子树,进行左旋。
- 插入
结点10
插入后没有破坏原来的平衡
- 插入
结点9
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oR5oJgVb-1590137686503)(https://user-gold-cdn.xitu.io/2020/5/22/1723a5a4ca9c683d?w=532&h=372&f=png&s=46088)]
结点7
的平衡因子为-2
,成为最小不平衡子树,进行左旋。此时结点9
比结点10
小,不能成为结点10
的右子树。
那么应该怎么办呢?
那么我们先总结一下上面的规律:
1. 平衡因子为负数,左旋
2. 平衡因子为正数,右旋
3. 平衡因子为有正负数,先旋转统一平衡因子的符号,进行双旋转
那么,我们可以先对结点10
和结点9
,进行一次右旋,如下:
此时,平衡因子的符号统一,结点7
的平衡因子
为-2
,然后再进行一次左旋,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IQ60Oawh-1590137686508)(https://user-gold-cdn.xitu.io/2020/5/22/1723a65a2250fc8a?w=560&h=362&f=png&s=46322)]
- 插入
结点8
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r8FqclrC-1590137686510)(https://user-gold-cdn.xitu.io/2020/5/22/1723a66db2414c67?w=555&h=434&f=png&s=60500)]
当插入结点8
时,出现了平衡因子符号不统一的情况,先进行右旋统一符号,得到如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n0dq8cqg-1590137686512)(https://user-gold-cdn.xitu.io/2020/5/22/1723a68ae34a55ef?w=618&h=445&f=png&s=63653)]
然后对结点6
,进行左旋。
最终数组a插入完毕,得到平衡二叉树。
旋转规律:
- 平衡因子为负数,左旋
- 平衡因子为正数,右旋
- 平衡因子为有正负数,先旋转统一平衡因子的符号,进行双旋转
2. 平衡二叉树构建
经过前面对平衡二叉树构建的分析,接下来分析一下具体怎么构建平衡二叉树。
首先,我们对二叉树
的二叉链表结点
结构,进行改造,加入平衡因子
属性。
二叉树
的二叉链表结点
结构如下:
//二叉树的二叉链表结点结构定义
//结点结构
typedef struct BiTNode{
//结点数据
int data;
//结点的平衡因子
int bf;
//结点左右孩子指针
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
当平衡二叉树节点需要右旋操作时:
如上图,插入节点N
时,破坏了原有的平衡,需要进行右旋,步骤如下:
1. 将 P 做为右旋的根节点
2. L 的右子树,即:Lr,成为 P 的左子树
3. P 成为 L 的右子树
4. L 替换原来的P,成为二叉排序树新的根节点
右旋的实现:
// 右旋
void R_Rotate(BiTree *p){
BiTree L;
//✅ L是p的左子树;
L = (*p)->lchild;
//✅ L的右子树作为p的左子树
(*p)->lchild = L->rchild;
//✅ 将p作为L的右子树
L->rchild = (*p);
//✅ 将L替换原有p的根结点位置
*p = L;
}
当平衡二叉树节点需要左旋操作时,其实和右旋是一样的思路:
1. P 作为左旋的根节点
2. R 的左子树,成为P的右子树
3. P成为R的左子树
4. R 替换 P 成为二叉树新的根节点
左旋代码实现:
// 左旋
void L_Rotate(BiTree *p){
BiTree R;
//✅ R是p的右子树
R = (*p)->rchild;
//✅ R的左子树作为R的右子树
(*p)->rchild = R->lchild;
//✅ 将p作为R的左子树;
R->lchild = (*p);
//✅ 将R替换原有p的根结点的位置
*p = R;
}
当需要双旋操作时,又分为两种情况,分别是左子树失衡需要双旋和右子树失衡需要双旋,我们可以先定义三个常量:
#define LH +1 /* 左高 */
#define EH 0 /* 等高 */
#define RH -1 /* 右高 */
-
左子树失衡需要双旋:
双旋前要先统一平衡因子的符号,然后先左旋,再右旋
-
右子树失衡需要双旋
双旋前要先统一平衡因子的符号,然后先右旋,再左旋
那么接下来看一下整体的平衡二叉树(AVL树) 的实现:
在平衡二叉排序树T中,不存在和插入元素e有相同关键字的结点,则插入一个数据元素为e的新节点,并返回1,否则返回0。若因为插入而是二叉排序树失衡,则做平衡旋转处理。变量taller反映 平衡二叉排序树T 是否长高。
构建思路:
-
- 判断 二叉排序树 T,是否为空,为空则创建一个新节点,此时,只有一个结点,其平衡因子BF为EH,表示等高,新结点,默认长高,
taller = TRUE
- 判断 二叉排序树 T,是否为空,为空则创建一个新节点,此时,只有一个结点,其平衡因子BF为EH,表示等高,新结点,默认长高,
-
- T 不为空,判断插入元素e是否等于结点T的数据,相等则返回
false
,默认taller = false
,不长高
- T 不为空,判断插入元素e是否等于结点T的数据,相等则返回
-
- 当插入元素e小于结点T的数据时,在T的左子树中进行搜索:
-
3.1 先递归判断是否成功插入,未插入,直接返回
false
-
3.2 判断成功插入到T的左子树中,且左子树长高时:
-
3.3 检查T的平衡度:
- 当T的BF为
LH(左高)
时,原本左子树比右子树高,需要做左平衡处理,此时taller = false
- 当T的BF为
EH(等高)
时,原本左子树和右子树等高,左子树增高而使树增高,此时taller = true
,T的BF为LH
- 当T的BF为
RH(右高)
时,原本右子树比左子树高,左子树增高而使左右子树等高,此时taller = false
,T的BF为EH
- 当T的BF为
-
- 当插入元素e大于结点T的数据时,在T的右子树中进行搜索:
-
4.1 先递归判断是否成功插入,未插入,直接返回
false
-
4.2 判断成功插入到T的左子树中,且右子树长高时:
-
4.3 检查T的平衡度:
- 当T的BF为
LH(左高)
时,原本左子树比右子树高,右子树增高而使左右子树等高,此时taller = false
,T的BF为EH
。 - 当T的BF为
EH(等高)
时,原本左子树和右子树等高,右子树增高而使树增高,此时taller = true
,T的BF为RH
。 - 当T的BF为
RH(右高)
时,原本右子树比左子树高,右子树增高而使树失衡,需要做右平衡处理,此时taller = false
。
- 当T的BF为
左平衡处理思路:
-
检查T的左子树的平衡度
L = T->lchild
,若其左子树L
的平衡度为LH
,新结点插入在T的左孩子L的左子树上,要作单右旋处理(右旋上面已经分析过) -
T的左子树的平衡度为
RH
,即:L->bf = RH
时,需要双旋处理,Lr
指向T的左孩子的右子树根,即:Lr=L->rchild
,修改T
及其左孩子
的平衡因子- 当
Lr
的BF
为LH
(左高)时,第一次左旋后,L平衡
,修改其BF
为EH(等高)
,由于T
之前是平衡的,进行旋转后修改T
的BF
为RH
。 - 当
Lr
的BF
为EH
(等高)时,第一次左旋后,L平衡
,修改其BF
为EH(等高)
,修改T
的BF
为EH
。 - 当
Lr
的BF
为RH
(右高)时,第一次左旋后,L平衡
,修改其BF
为LH(左高)
,修改T
的BF
为EH
。
- 当
-
修改
Lr
的BF
为EH
-
对
T的左子树
做左旋处理 -
对
T
做右旋处理
右平衡处理思路:
-
检查T的右子树的平衡度
R = T->rchild
,若其右子树R
的平衡度为RH
,新结点插入在T的右孩子R的右子树上,要作单左旋处理(左旋上面已经分析过) -
T的右子树的平衡度为
LH
,即:R->bf = LH
时,需要双旋处理,Rl
指向T的右孩子的左子树,即:Rl=R->lchild
,修改T
及其右孩子
的平衡因子- 当
Rl
的BF
为LH
(左高)时,第一次右旋后,R平衡
,修改其BF
为RH(等高)
,由于T
之前是平衡的,进行旋转后修改T
的BF
为EH
。 - 当
Rl
的BF
为EH
(等高)时,第一次右旋后,R平衡
,修改其BF
为EH(等高)
,修改T
的BF
为EH
。 - 当
Rl
的BF
为RH
(右高)时,第一次右旋后,R平衡
,修改其BF
为EH(等高)
,修改T
的BF
为LH
。
- 当
-
修改
Rl
的BF
为EH
-
对
T的右子树
做右旋处理 -
对
T
做左旋处理
Status InsertAVL(BiTree *T,int e,Status *taller)
{
if(!*T)
{ //1.插入新结点,树“长高”,置taller为TRUE
//① 开辟一个新结点T;
*T=(BiTree)malloc(sizeof(BiTNode));
//② 对新结点T的data赋值,并且让其左右孩子指向为空,T的BF值为EH;
(*T)->data=e;
(*T)->lchild=(*T)->rchild=NULL;
(*T)->bf=EH;
//③ 新结点默认"长高"
*taller=TRUE;
}
else
{
if (e==(*T)->data)
{ //2.树中已存在和e有相同关键字的结点则不再插入
*taller=FALSE;
return FALSE;
}
if (e<(*T)->data)
{
//3.应继续在T的左子树中进行搜索
if(!InsertAVL(&(*T)->lchild,e,taller))
//未插入
return FALSE;
//4.已插入到T的左子树中且左子树“长高”
if(*taller)
//5.检查T的平衡度
switch((*T)->bf)
{
case LH:
//原本左子树比右子树高,需要作左平衡处理
LeftBalance(T);
*taller=FALSE;
break;
case EH:
//原本左、右子树等高,现因左子树增高而使树增高
(*T)->bf=LH;
*taller=TRUE;
break;
case RH:
//原本右子树比左子树高,现左、右子树等高
(*T)->bf=EH;
*taller=FALSE;
break;
}
}
else
{ //6.应继续在T的右子树中进行搜索
//未插入
if(!InsertAVL(&(*T)->rchild,e,taller))
return FALSE;
//已插入到T的右子树且右子树“长高”
if(*taller)
// 检查T的平衡度
switch((*T)->bf)
{
//原本左子树比右子树高,现左、右子树等高
case LH:
(*T)->bf=EH;
*taller=FALSE;
break;
//原本左、右子树等高,现因右子树增高而使树增高
case EH:
(*T)->bf=RH;
*taller=TRUE;
break;
// 原本右子树比左子树高,需要作右平衡处理
case RH:
RightBalance(T);
*taller=FALSE;
break;
}
}
}
return TRUE;
}
// 左平衡树失衡处理
void LeftBalance(BiTree *T)
{
BiTree L,Lr;
//1.L指向T的左子树根结点
L=(*T)->lchild;
//2.检查T的左子树的平衡度,并作相应平衡处理
switch(L->bf)
{
//① 新结点插入在T的左孩子的左子树上,要作单右旋处理(如图1-平衡二叉树右旋解释图)
case LH:
//L的平衡因子为LH,即为1时,表示它与根结点BF符合相同,则将它们(T,L)的BF值都改为EH(0)
(*T)->bf=L->bf=EH;
//对最小不平衡子树T进行右旋;
R_Rotate(T);
break;
//② LH的平衡因子为RH(-1)时,它与跟结点的BF值符合相反.此时需要做双旋处理(2次旋转处理)
// 新结点插入在T的左孩子的右子树上,要作 双旋处理
case RH:
//Lr指向T的左孩子的右子树根
Lr=L->rchild;
//修改T及其左孩子的平衡因子
switch(Lr->bf)
{
case LH:
(*T)->bf=RH;
L->bf=EH;
break;
case EH:
(*T)->bf=L->bf=EH;
break;
case RH:
(*T)->bf=EH;
L->bf=LH;
break;
}
Lr->bf=EH;
//对T的左子树作左旋平衡处理
L_Rotate(&(*T)->lchild);
//对T作右旋平衡处理
R_Rotate(T);
}
}
// 右平衡树失衡处理
void RightBalance(BiTree *T)
{
BiTree R,Rl;
//1.R指向T的右子树根结点
R=(*T)->rchild;
//2. 检查T的右子树的平衡度,并作相应平衡处理
switch(R->bf)
{
//① 新结点插入在T的右孩子的右子树上,要作单左旋处理
case RH:
(*T)->bf=R->bf=EH;
L_Rotate(T);
break;
//新结点插入在T的右孩子的左子树上,要作双旋处理
case LH:
//Rl指向T的右孩子的左子树根
Rl=R->lchild;
//修改T及其右孩子的平衡因子
switch(Rl->bf)
{
case RH:
(*T)->bf=LH;
R->bf=EH;
break;
case EH:
(*T)->bf=R->bf=EH;
break;
case LH:
(*T)->bf=EH;
R->bf=RH;
break;
}
Rl->bf=EH;
//对T的右子树作右旋平衡处理
R_Rotate(&(*T)->rchild);
//对T作左旋平衡处理
L_Rotate(T);
}
}
个人见解,如有错误,欢迎指正。