平衡二叉树是一种特殊的二叉排序树,其定义:为空树或者
它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度差的绝对值不超过1
如图,第三个就是错的,因为7的右子树高度为0,左子树高为2,BF(T)= 2不满足要求
h=3、4、5类似,谨记:树的左右子树依然是平衡二叉树
通过推算能得出:当一个结点数量为N组成的平衡二叉树,其高度能达到logn
平衡二叉树的调整:
RR旋转
对于Mar,May组成的二叉树,然后准备插入Nov,如果其插入May的子树上,那么这就无法构成一个平衡二叉树。
是由于Nov连上了May,导致Mar(发现者)的高度差不满足平衡二叉树的定义,因而Nov(麻烦结点)破坏了树的结构,若想平衡起来,很自然想到组成一个头结点,再一个左右子树就满足需求了,于是可以通过对原树进行一次右单旋,变成右边的结构。
抽象化说明:
对于一个已经平衡的二叉树(图①),在BR位置插入一个节点,变成图②所示,导致这棵树变得不平衡,“麻烦结点”是新节点,“发现者”是A,位置在A右子树的右边,因此进行RR旋转,物理上是将其单旋一次,变成图③的模样。
(别忘了,这也是一个查找树。视频并未提及平衡二叉树和查找树的关系,但请记住,平衡二叉树是一种特殊的二叉排序树)对于BL,由于B是A的右子树,因此BL和BR元素值肯定大于A,因此可以放到A的右子树上。
这也是RR旋转,依然满足“破坏者”在“发现者”右子树(10)的右边(14)
LL旋转
插入Apr,导致Mar,May不平衡,此时,作为距离Apr最近的点为“发现者”,在发现者-问题节点之间进行左旋,由于这次Apr是在Mar左子树的左边,因此称为LL旋转
抽象化说明:类似RR旋转,不多赘述。
对于BR,B是A的左子树,所以BR肯定小于A,因此在旋转后可以放到A的左子树
接着插入Jan。根据查找树的规则,Jan在Mar的左子树上,这样导致May的平衡被破坏。
此时Jan在May左子树的右子树上,因此这被称为LR旋转。
LR旋转所注意的关键点为May(被破坏点)~Aug ~Mar这三个节点(顺序为破坏者,破坏者的L,破坏者的LR),把这三个节点调平衡了也就调整完毕。
要做成这样,显然头结点是需要这三个节点的中间值,在题目中就是Mar了。然后左边为比它小的(Aug),右边比它大(May),这样就能变成一个平衡二叉树(如图②)
抽象概述:
在CL或者CR中新增一个节点,导致A不平衡(不平衡点在A的左子树的右子树,即LR),因此需要在ABC(即A,AL,ALR)这三个点进行LR旋转。
这样还多出CL和CR,由于B<C,A>C,且CL<C,CR>C能得出:CL肯定比B大,CR肯定比A要小,因此CL连到B的右子树,CR连到B的左子树上,即图③。
RL旋转:
破坏者(Feb)是在被破坏者(Aug)的右子树的左子树上,因而进行旋转称为RL旋转
过程与LR旋转类似,不多赘述。
继续插入:
插入June,Mar被破坏,因而需要对Mar,MarL(Dec),MarLR(Jan)进行调整,也就是LR旋转
接着插入Oct,显然,RR旋转
至此为RR,LL,LR,RL旋转的基本概念
2019年4月10日新增代码实现:
要构建一个平衡二叉树,最难的就是在每次插入数据后所需要进行的判断LL,LR,RR,RL旋转
AVL树的定义:其内部必须得有一个“height”来监控每一个节点的子树高度差是否违反要求,用链表为存储结构,其定义如下:
typedef struct AVLNode *AVLTree; struct AVLNode{ int value; AVLTree left,right; int height; };
链表的创建:正常操作,代码下文一并附上
节点的插入:谨记平衡二叉树的定义:它是一种特殊的二叉搜索树,其左右子树的高度差不超过1,因此在初步插入节点时它的代码结构和二叉搜索树的插入相同:
AVLTree Insert(AVLTree T,int a){ if(!T){//T为可插入的空节点 T = new AVLNode(); T->value = a; T->left = T->right = NULL; T->flag = 0; }else{ if(a>T->value) T->right = Insert(T->right,a); else if(a<T->value) T->left = Insert(T->left,a); } return T; }
拿其中一个插入为例,在T->right处插入数据,根据递归会一直到能够插入节点的那个地方然后创建节点,再返回。成功插入后需要逐层返回判断:①此时是否有节点的高度差不满足,即高度差为2的那个节点;②如果满足,应该更新这个节点的高度差。根据自底向上的顺序,肯定会先有正常更新节点的节点,再会有异常高度差节点,因此先解决如何更新高度差:
注:12345均为结点名,方便表述,并不是值
我们规定,④为新插入的节点,并且深度差为左减右
对于④,很容易明白,其height为0
然后递归返回到③,显然③的深度是1(肉眼观察)。在操作上,这时候③的深度是③的左子树和右子树之差,即T->left和right之差,显然这是1 - 0 = 1,因此标为1,但是在数据上,③的左子树的height为0,③的右子树为空,因此③深度的计算应该是0 - x = 1,很好得出x = -1,因此可以规定:如果子树为空,那么它的height = -1
递归返回到②,根据上述结论,②的深度是 1 - (-1) = 2,需要进行旋转操作。
先不要考虑旋转操作,我们再举个例子。
同样照搬之前的规定,⑤为新插入节点,height = 0
④的深度应该为 -1 - 0 = -1
③的深度应该为 -1 - (-1) = 0,这样就出问题了,如果在左插入右插入时规定的作差方法还不一样,这样代码实现起来会复杂很多很多,因此这里有一个新思路,我们规定深度都为正数,这样④的深度为1,在③中的操作也就是-1 - 1 = -2,绝对值一下就知道这个深度大于1,虽然-2依然是负数,但这个是会立即进行操作的,在“判断旋转类型”中马上会提及到,不用担心。
那么对于这更新深度的总结:
①如果这个树是叶节点,那么深度就为0
②如果子树为空,其深度(在计算上)应该为-1
③树枝节点的值应该保证其一直为正数,方便接下来的操作
因此,首先,我们需要一个函数GetHeight()来获取其深度
int GetHeight(AVLTree T){ if(!T) return -1; return T->height; }
再考虑左减右为负数的情况下,我们要令它的深度保持一个正数,在所有能够正常返回的深度中,配对的有 -1 - 0 =1;1 - 0 = 1; 0 - (-1) = 1; (1 - 1 = 0这种形式不会出现),可以选用其中的最大值进行+1,也能得到我们想要的深度(这一块我解释的不是太好,希望有大佬能帮忙补充)
T->height = Max(GetHeight(T->left), GetHeight(T->right)) + 1;
回到上文:如何判断这个是RR还是LL还是RL、LR类型的旋转呢?
判断旋转类型:
由于最初我们规定的深度差是左减右,根据异常值2或者-2能看出,这是左子树太重,出了问题,因而至少能知道这个是L_旋转。在这里再回顾一下LL和LR的定义:LL发生在左子树的左子树,LR发生在左子树的右子树,这个异常节点肯定要么在其发现者左子树的右子树或者左子树,也就是判断异常节点和发现节点左子树的大小关系,如果大,那么就是LR旋转,如果小,那么就是LL旋转,反之对于RR,RL是相同的道理。
if(!T){ T = new AVLNode(); T->value = a; T->left = T->right = NULL; T->height = 0; } else if(a > T->value){ T->right = Insert(T->right,a); if(GetHeight(T->left) - GetHeight(T->right) == -2){//右子树更重,因此是R_旋转 if(a > T->right->value) T = RR_Rotation(T);//a比右子树更大,即a所放置的地方在右边,因此执行RR旋转 else T = RL_Rotation(T);//反之,a所放置的地方在左子树的左边,因此执行RL旋转,下同 } }else if(a < T->value){ T->left = Insert(T->left,a); if(GetHeight(T->left) - GetHeight(T->right) == 2){ if(a < T->left->value) T = LL_Rotation(T); else T = LR_Rotation(T); } }
接下来就是对旋转操作的代码实现:
对RR旋转为例:
A是发现者,因此是以A节点为起点进行操作(其它旋转都是以发现者为起点进行操作),不难看出是把BL连上了A的右儿子,A本身连上了A的左儿子(没错就是那么简单粗暴),重新连接完毕后不要忘了更新节点的深度
AVLTree RR_Rotation(AVLTree T){ AVLTree AVT = T->right; T->right = AVT->left; AVT->left = T; T->height = Max(GetHeight(T->left), GetHeight(T->right)) + 1; AVT->height = Max(GetHeight(AVT->left), T->height) + 1; return AVT; }
对于RL以及LR旋转,以RL为例:
不难看出,是将C变为头结点,CL变为A的右儿子,CR变为B的左儿子,直接根据所说的进行操作也可以,这里提供浙大数据结构所提供的代码思路:
以先序遍历为准,最初的顺序为ABC,后来变成了CAB,可以看成C一步一步往右挪动,变成根节点。
那么我们可以对BC先进行旋转,C是B的左子树,因此进行一次LL旋转,可以粗糙理解为C和B换了一个位置,此时顺序为ACB,再对AC进行旋转,此时C是A的右子树,因此进行一次RR旋转,这样也能得到想要的结果。
也就是说,RL旋转 = LL旋转 + RR旋转(顺序是反的!)
AVLTree RL_Rotation(AVLTree T){ T->right = LL_Rotation(T->right); return RR_Rotation(T); }
LL和LR旋转代码及思路类似。
最后附上所有代码,根据Root of AVL Tree改写而来:
#include<bits/stdc++.h> using namespace std; typedef struct AVLNode *AVLTree; struct AVLNode{ int value; AVLTree left,right; int height; }; int Max(int a,int b); AVLTree LL_Rotation(AVLTree T); AVLTree LR_Rotation(AVLTree T); AVLTree RR_Rotation(AVLTree T); AVLTree RL_Rotation(AVLTree T); AVLTree CreateTree(int n); AVLTree Insert(AVLTree T,int a); int GetHeight(AVLTree T);//需注意,很重要 void ReadTree(AVLTree T); int GetHeight(AVLTree T){ if(!T) return -1; return T->height; } AVLTree CreateTree(int n){ int a; AVLTree T = new AVLNode(); cin>>a; T->value = a; T->left = T->right = NULL; T->height = 0; for(int i = 1; i < n; i++){ cin>>a; T = Insert(T,a); } return T; } AVLTree Insert(AVLTree T,int a){ if(!T){ T = new AVLNode(); T->value = a; T->left = T->right = NULL; T->height = 0; } else if(a > T->value){ T->right = Insert(T->right,a); if(GetHeight(T->left) - GetHeight(T->right) == -2){//右子树更重,因此是R_旋转 if(a > T->right->value) T = RR_Rotation(T);//a比右子树更大,即a所放置的地方在右边,因此执行RR旋转 else T = RL_Rotation(T);//反之,a所放置的地方在左子树的左边,因此执行RL旋转,下同 } }else if(a < T->value){ T->left = Insert(T->left,a); if(GetHeight(T->left) - GetHeight(T->right) == 2){ if(a < T->left->value) T = LL_Rotation(T); else T = LR_Rotation(T); } } T->height = Max(GetHeight(T->left), GetHeight(T->right)) + 1; return T; } int Max(int a,int b){ return a>b?a:b; } AVLTree LL_Rotation(AVLTree T){ AVLTree AVT = T->left;//LL旋转,即让B的右子树连到A的左子树,B的左子树变成A T->left = AVT->right; AVT->right = T; T->height = Max(GetHeight(T->left), GetHeight(T->right)) + 1; AVT->height = Max(GetHeight(AVT->left), T->height) + 1; return AVT; } AVLTree LR_Rotation(AVLTree T){ T->left = RR_Rotation(T->left);//如ABC三个进行LR旋转,结果是CBA(先序遍历顺序,下同),可先对BC进行RR旋转,也就是BC换位 return LL_Rotation(T);//BC换位成功后顺序是ACB,再对AC进行LL旋转,也就是AC换位,这样就出来CBA了 } AVLTree RR_Rotation(AVLTree T){ AVLTree AVT = T->right; T->right = AVT->left; AVT->left = T; T->height = Max(GetHeight(T->left), GetHeight(T->right)) + 1; AVT->height = Max(GetHeight(AVT->left), T->height) + 1; return AVT; } AVLTree RL_Rotation(AVLTree T){ T->right = LL_Rotation(T->right); return RR_Rotation(T); } void ReadTree(AVLTree T){ if(T){ cout<<T->value<<' '; ReadTree(T->left); ReadTree(T->right); } else return; } int main(){ int n; cin>>n; AVLTree T = CreateTree(n); ReadTree(T); // cout<<T->value<<endl; return 0; }