文章目录
只有AVL树真正的平衡了,我的心才会平衡。花了很长的时间,掉了很多的头发才将平衡查找树给弄明白。此时此刻我愿意再掉点头发,看能否给大家讲明白。
本文适合对二叉排序树(二叉搜索树、二叉查找树)已经有过学习的读者。
一.前言
声明一点:因为树的高度存在两个版本的定义,此处树高度的定义采用版本一,即将根结点所在的位置定义为第一层。详细细节请阅读我往期文章。
如果向一棵二叉查找树输入预先排序好的数据,那么这棵二叉查找树要么只有左子树,要么只有右子树,变成线性的了。其查找效率由O(log(n))退化为O(n),因此这样的树进行插入操作的代价会变得很大。
例如:
插入数据:5、4、3、2、1
根据二叉查找树的性质得以下图:
该树构建出来之后只有左子树,左子树的高度为4,右子树的高度为0,整棵树的高度为5,成了线性结构,如果还要插入数据“0”,就要遍历到左子树的最后一个结点进行插入,从而没有体现树形结构的优越性。
再例如:
插入数据:1、2、3、4、5
根据二叉查找树的性质得:
该树构建出来之后只有右子树,同样变成线性的了。那我们应该如何让结点进行插入的时候依然保持树结构的查找效率为O(logN)呢?也就是继续保持二叉查找树的优点。
这就引入了平衡二叉树。
二.平衡二叉树
平衡二叉树的本质还是二叉查找树,它是二叉查找树的改进版。平衡二叉树就是在二叉查找树的基础上再加一条性质:左子树和右子树的高度之差的绝对值不超过1。这就是平衡的条件。
而往往导致不平衡的原因就是,新插入的结点导致左右子树的高度差大于1。根据插入的位置不同,导致这种不平衡的原因可能出现在下面的四种情况中:
2.1左左(LL)
设需要重新平衡的结点叫做N结点。
当对N结点的左子树的左子树进行一次插入时导致了不平衡,有三类如下:
对于这三种情况都是左子树的高度与右子树的高度的差值大于1,需要对红色标记的结点进行调整。面对LL的这三种类型该如何处理?通过一次右旋。
以下的思想只是为了讲清楚如何进行调整,也是为了生动些,若有些夸张请见谅。但是读者心中时时刻刻都一定要有二叉查找树的性质。
前方高能!!前方高能!!
第一类:
如何让这棵树恢复为平衡二叉树?方法:一次右旋。
假设将这三个结点看为三个同学,它们共处于图书馆中进行学习。但是图书馆中有规定,同学之间不能坐的太分散必须集中一点。此时3号同学发现2号同学的右边有一个位置没有人坐,他也不想舍近求远,于是它就把那个位置给占了,如下图:
此时符合了图书馆的要求,也符合了平衡二叉树的要求
第二类:
如何让这棵树恢复为平衡二叉树?方法:一次右旋。
假设将这几个结点看作几个同学,它们共处于图书馆中学习。但馆中有规定,同学之间不能坐的太分散。但是呢谁都不想动,就这样一直耗着。终于4号结点耐不住了,想去厕所,如下图:
此时图书管理员又发话了,叫进行调整。6号同学想了想,算了吧我动一下。此时它发现3号同学右边没有人,于是叫上了它的好兄弟一起来到了3号同学的右边(此时4号同学还在上厕所),如下图:
当4号同学回来了之后,发现自己的位置被6号同学给占了。但4号同学还想坐回原位,所以他坐到了6号同学左边的位置等待6号同学的离开,如下图:
此时即满足了图书馆的要求,也满足了平衡二叉树的要求
第三类:
如何让这棵树恢复为平衡二叉树?方法:一次右旋。
同样假设将这几个结点看作几个同学,它们共处于图书馆中学习。图书馆规定,同学之间不能坐的太分散。依然此时谁都不想动,就这样一直耗着。突然5号同学想去厕所,如下图:
同样的故事又上演了。6号同学想了想,算了吧我动一下。此时它发现4号同学右边没有人,叫上了他的好兄弟一起来到了4号同学的右边(此时5号同学还在上厕所),如下图:
当5号同学回来了之后,发现自己的位置被6号同学给占了。同样的故事,同样的想法,同样的结局,如下图:
这样图书馆又恢复到了往日的氛围,平衡二叉树又恢复到了往日的平衡。
2.2右右(RR)
设需要重新平衡的结点叫做N结点。
当对N结点的右子树的右子树进行一次插入时导致了不平衡。这种情况和左左(LL)的情况是类似的,给出它的三类情况,我就不重复讲故事了。
进行调整的方法是:通过一次左旋
2.3左右(LR)
设需要重新平衡的结点叫做N结点。
当对N结点的左子树的右子树进行一次插入时导致了不平衡,有三类如下:
如上图需要对红色标记的结点进行调整,才能保证满足平衡二叉树的要求。面对LR的这三种类型该如何处理?先左旋,再右旋。
平衡二叉树还没有平衡,图书馆的小故事那就还得继续上演
前方高能!!
第一类:
如何将这棵树恢复为平衡二叉树?方法:先左旋,再右旋
假设将这几个结点看作在图书馆进行学习的同学。图书馆还是那个图书馆,规定还是那个规定。可能2号同学是第一次来到图书馆,还不知道规定,他只是感觉好像这样坐不行。此时2号同学看了看前面,发现3号同学旁边有一个位置空着,于是他就走过去把那个位置给占了,如下:左旋
3号同学先是愣了愣,好像不对呀,我们先在更分散了。于是3号同学叫着2号同学一起来到了4号同学的左边,如下:
但是图书管理员又发话了,叫按规定坐。此时2号同学和3号同学都不知所措了。4号同学想了想,算了吧还是我来吧(4号同学是这里的常客,熟悉规定。大家有没有发现,现在这棵树的状态就变为了LL–左左的情况),于是4号同学就来到了3号同学的右边,如下:右旋
终于满足了图书馆的规定,也终于满足了平衡二叉树的要求。
第二类:
如何将这棵树恢复为平衡二叉树?方法:先左旋,再右旋
同样假设将这几个结点看作在图书馆进行学习的同学。图书馆的规定还是没有变。3号同学和2号同学是第一次来到图书馆,还不知道规定,他只是感觉这样坐不行。此时3号同学看了看前面,发现4号同学旁边有一个位置空着,于是打破了图书馆的安静,叫上2号同学一起把那个位置给占了,如下:左旋
4号同学愣了一下说:“不对,我们先在更分散了”。于是4号同学叫着他周围的同学一起来到了6号同学的左边,如下:
(细心的读者可能已经发现了,此时又变为了LL–左左的情况)故事继续:
管理员又喊话了,但是呢此时谁都不愿意动。当管理者再一次喊话时,就这样厕所事件又发生了。5号同学去上厕所后,位置被6号同学和他的好兄弟给占了,如下:左旋
5号同学回来后,发现位置被占了,也想坐回原来的位置,也就坐到了6号同学的左边,等待6号同学的离开,如下:
终于图书馆和谐了,平衡二叉树也平衡了。
第三类:
又如何将这棵树恢复为平衡二叉树?方法:先左旋,再右旋
同样假设将这几个结点看作在图书馆进行学习的同学,图书馆的规定大家都知道了吧。3号同学以前来过图书馆,但还不太熟悉,不过他还是愿意做第一个改变的。当4号同学去上厕所的时候他叫上他的好兄弟2号同学把4号同学的位置给占了,如下:左旋
同样4号同学回来后,发现位置被占了,依旧想回到原来的位置,于是坐到了3号同学的右边,如下:
此时5号同学终于主动站起来组织他周围的同学进行调整,如下:
(细心的同学不知道有没有再一次发现又变为了LL–左左的情况。)
此时管理员叹了一口气,直接快无语了,同学们还是没有按照规定来。6号同学看了看心想,还是我来吧。于是叫上了他右边的同学,一起来到了5号同学的右边,如下:右旋
终于图书管理员露出了欣慰的笑容,心想你们终于搞懂图书馆的规定了。平衡二叉树也笑了笑,你终于知道怎么样保证我的平衡了。
2.4右左(RL)
设需要重新平衡的结点叫做N结点。
当对N结点的右子树的左子树进行一次插入时导致了不平衡。这种情况和左右(LR)的情况是类似的,给出它的三类情况,我也不重复讲故事了。
三.平衡二叉树进行插入的代码设计
其实上面的故事中已经蕴含了代码的思想,不知道读者有没有发现。这四种情况中,每一种情况都有三小类,这是代码实现的细节,需要考虑周到的一些点。对于后面的两种情况(LR和RL),可以单独为它们设计代码,也可以通过调用前两种情况的代码来实现(比如LR:可以先通过左旋,再通过右旋)只是传参的参数不一样,这得多思考一下。
在这里我就只说一下左左(LL)和左右(LR)的情况,其它两种都是对应类似的,不重复赘述。
3.1左左(LL)代码设计
以第一类的数据为例,设3号结点为Father结点,Child=Father->lChild(2号结点)。
Father->lChild=Child->rChild(将2号结点的右子树,接到3号结点的左子树中)。
Child->rChild=Father(将3号结点接到2号结点的右子树中),该树平衡,整个过程如下图:
3.2左右(LR)代码设计
前面也说过,对于这种情况可以通过调用RR和LL情况的代码来实现,但是在这里还是写一下吧。
以第一类的数据为例,设4号结点为Father结点,Child=Father->lChild(2号结点);Grandson=Child->rChild(3号结点)。
Child->rChild=Grandson->lChild(将3号结点的左子树接到2号结点的右子树中)。
Grandson->lChild=Child(将2号结点接入到3号结点的左子树中)。
Father->lChild=Grandson->rChild(将3号结点的右子树接入到4号结点的左子树中)。
Grandson->rChild=Father(将4号结点接入到3号结点的右子树中),该树平衡。整个过程如下图:
四.平衡二叉树进行插入的代码实现及测试
4.1代码实现
思路:
1.建立平衡二叉树的过程就是建立一棵二叉查找树的过程
2.在建立的过程中我们需要去进行调整,调整需要用到树的高度,因此需要计算出树的高度。
3.写调整方法
整体分三部分来完成:
1.写一个接口函数来求树的高度;
2.写四个调整方法的接口函数;
3.写平衡二叉树的插入方法。
设树的结构体如下:
typedef int TreeNodeDataType;//树结点的数据类型
typedef struct TreeNode
{
TreeNodeDataType data;//结点数据
struct TreeNode* lChild;//左孩子
struct TreeNode* rChild;//右孩子
}TreeNode;
求树高度的接口函数如下:
int GetHeight(TreeNode* T)
{
int lHeight = 0;//记录左子树的高度
int rHeight = 0;//记录右子树的高度
if (T == NULL)return 0;//空树高度为0
else
{
lHeight = GetHeight(T->lChild);//递归遍历左子树
rHeight = GetHeight(T->rChild);//递归遍历右子树
return lHeight > rHeight ? lHeight + 1 : rHeight + 1;//返回左右子树最大值加1(还要加上根结点)
}
}
调整方法的接口函数如下:
LL(左左)情况:
void LL_Rotation(TreeNode* father, TreeNode** root)//第一个参数为值传递,第二个参数为指针参数传递
{
TreeNode* Child = father->lChild;
father->lChild = Child->rChild;//右旋
Child->rChild = father;//右旋变为自己的右孩子
*root = Child;//将原来的根结点进行改变
}
RR(右右)情况:
void RR_Rotation(TreeNode* father, TreeNode** root)//第一个参数为值传递,第二个参数为指针参数传递
{
TreeNode* Child = father->rChild;
father->rChild = Child->lChild;//左旋
Child->lChild = father;//左旋就变为自己的左儿子
*root = Child;//将原来的根结点进行改变
}
LR(左右)情况:
void LR_Rotation(TreeNode* father, TreeNode** root)//第一个参数为值传递,第二个参数为指针参数传递
{
TreeNode* Child = father->lChild;
TreeNode* Grandson = Child->rChild;
Child->rChild = Grandson->lChild;//先左旋
Grandson->lChild = Child;//左旋为自己的左儿子
father->lChild = Grandson->rChild;//再右旋
Grandson->rChild = father;//右旋为自己的右孩子
*root = Grandson;//将原来的根结点进行改变
}
RL(右左)情况:
void RL_Rotation(TreeNode* father, TreeNode** root)//第一个参数为值传递,第二个参数为指针参数传递
{
TreeNode* Child = father->rChild;
TreeNode* Grandson = Child->lChild;
Child->lChild = Grandson->rChild;//先右旋
Grandson->rChild = Child;//右旋为自己的右儿子
father->rChild = Grandson->lChild;//再左旋
Grandson->lChild = father;//左旋为自己的左儿子
*root = Grandson;//将原来的根结点进行改变
}
平衡二叉树的插入方法如下:
void Insert(TreeNode** T, TreeNodeDataType e)
{
if (*T == NULL)
{
*T = (TreeNode*)malloc(sizeof(TreeNode));
assert(*T);
(*T)->data = e;
(*T)->lChild = NULL;
(*T)->rChild = NULL;
}
else if (e < (*T)->data)//在左子树进行插入
{
Insert(&(*T)->lChild, e);
//获取树的高度
int lHeight = GetHeight((*T)->lChild);
int rHeight = GetHeight((*T)->rChild);
//判断左右子树的高度差
if (lHeight - rHeight > 1)
{
if (e < (*T)->lChild->data)//LL(左左调整)
{
LL_Rotation(*T, T);
}
else //LR(左右调整)
{
RR_Rotation((*T)->lChild, &(*T)->lChild);//先左旋,注意参数
LL_Rotation(*T, T);//再右旋
//LR_Rotation(*T, T);
}
}
}
else if (e > (*T)->data)//在右子树进行插入
{
Insert(&(*T)->rChild, e);
//获取左右子树的高度
int lHeight = GetHeight((*T)->lChild);
int rHeight = GetHeight((*T)->rChild);
//计算高度差
if (rHeight - lHeight > 1)//判断高度差有没有大于1
{
if (e > (*T)->rChild->data)//RR(右右调整)
{
RR_Rotation(*T, T);
}
else//RL(右左调整)
{
LL_Rotation((*T)->rChild, &(*T)->rChild);//先右旋,注意参数的区别
RR_Rotation(*T, T);//再左旋
//RL_Rotation(*T, T);
}
}
}
}
4.2代码测试
采用先序遍历其接口函数如下:
void PreorderTraversal(TreeNode* T)//先序遍历
{
if (T != NULL)
{
std::cout << T->data << " ";
PreorderTraversal(T->lChild);
PreorderTraversal(T->rChild);
}
}
测试函数如下:
int main()
{
TreeNode* T = NULL;
int num[]{ 1,2,3,4,5,6,7 };
for (int i = 0; i < 7; i++)
{
Insert(&T, num[i]);
}
PreorderTraversal(T);
}
测试结果如下:
构图如下:
多次测试依旧有效,代码设计成功。
五.总结
如果遇见多棵树不平衡,要先选择最小树,只有保证了所有的小树是平衡二叉树才能保证这整棵二叉排序树是平衡二叉树。
有关平衡查找树的知识在这里我也不多说了,我也怕大家眼睛疼嘛。每一次我写完一篇博客我都会仔细读上好几遍,就怕因为自己给误导了大家。也希望有问题大家帮我指出,我会及时纠正。
给读者比个 ❤️
我是“老胡”,感谢阅读!!