小引
使用二叉排序树是为了降低查找复杂度,即使一个杂乱无章的顺序有更快的查找速率,降低时间复杂度
但如果我们的数据构成了如下的二叉排序树
其查找效率就变成了O(n),因为查找每一个元素都需要查找其在树的深度的位置,相当于查找一个顺序序列的线性表
O(n) = O(1/n * (n * (n+1)/2)) = O(n)
那我们也只是达成了查找的目的,没有达到降低查找效率的问题
所以我们应该找到方法去解决这种效率性的问题,从而引出平衡二叉树的操作
由二叉排序树到平衡二叉树的转换
根据上述的分析我们也可以得到一些知识,结合一下之前树的定义。我们找一个树上的节点,所要找寻的次数,等于该节点在此树上的层次或者深度,那么如果我们按照满二叉树的编号来对上面的树进行对应,并且以此计算树的深度,会达成一些更加系统化的深度计算
深度h = log2n(树节点编号) + 1
那如果我们可以将上面的这棵树,尽可能的还原成一棵满二叉树(或者完全二叉树),那么每个节点的深度累加之和是不是就变成最小的了?
答案是肯定的,因为其尽可能的让树的深度变得最低,自然在该树上的查找效率就会被放的最大。
如何改变树的结构,使其成为一棵效率最大的树?
先简述平衡二叉树的定义(引用):
平衡二叉查找树:简称平衡二叉树。由前苏联的数学家Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为AVL 树。它具有如下几个性质: 1、可以是空树。 2、假如不是空树,任何一个节点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过1。平衡之意,如天平,即两边的分量大约相同。
那么AVL树就是一种基于二叉排序树的树,每个节点的平衡因子不超过1
什么是平衡因子?
这里我们介绍什么是平衡因子。平衡因子应该被当做AVL树中每个节点自身的性质,其用于说明某节点的最偶有字数的高度差的绝对值
这就发展到了结构功能上,因为既然要打造满二叉树,则我们通过研究其性质能够得知,树中的每一个节点的左右子树差值是不超过1的,那么我们就以此为条件,改造原来的二叉搜索树
判断一个二叉排序树是否平衡,就是判断每个节点的平衡满足情况
而在一棵二叉搜索树中,有可能不止一个节点失衡,而是多个,针对该问题我们应该如何去决断?引入一个新的概念,最小不平衡子树
什么是最小不平衡子树?
我们每次插入一个新数据节点后,可能会造成其上面的树节点的失衡,可能是一个,可能是多个,而我们需要做的就是找到第一个失衡的节点,调整以该接节点为根节点的数的状态,即可使整棵树平衡,而被调整的树就可以称为最小不平衡子树
而基于最小不平衡子树我们应该怎么进行调整?一个比较抽象的方法是旋转
如上述二叉树,插入节点99后如何进行操作?将77节点作为整棵树的根节点,66节点带着其孩子作为77节点的新的左孩子,而77节点的原左孩子72节点作为66节点的新右孩子,整体上抛去对77节点左孩子的判断的话,就像是这棵树以节点77为轴整体向左旋转了一下,这个过程被称为左旋
由于不平衡的形态不同(类似上述为例,是左右左失衡),我们针对不同的形态有不同的方式将其修改为我们想要的AVL树(保证每个树节点的平衡因子值不超过1),该过程没有视频讲解来得舒服,建议听哔站青岛大学王卓老师讲得数据结构与算法基础课的平衡二叉树部分,下面呢我们不以树结构分析如何进行树的结构变换操作,而是以操作入手进行分析
构造AVL树的操作分析
左旋操作:x节点失衡,对x进行左旋调整(右>左,指该失衡节点左右层数)
-
y = x->right(新指针单独标记x的右孩子并命名为y节点)
-
x->right = y->left
-
y->left = x
-
return y(返回新的根节点)
树插入节点后造成不平衡:
操作2进行x节点指针指向的转换,x的右孩子成为y的左孩子:
操作3进行y节点左孩子的重新赋值,即左孩子成为了x节点:
左旋平衡后的AVL树:
综上有什么节点调换呢?
原根节点变成了其右孩子的左子树,而原根节点右孩子的左孩子,变成了原根节点的右子树,此过程就是节点位置转换(原根节点和原根节点的右孩子),然后其左右孩子也跟着转换(原根节点的右孩子变为原根节点的右孩子的左孩子,原根节点的右孩子的左孩子,变成原根节点)
那么看过王卓老师课程的同学都清楚了AVL树的四种不平衡调换形态,在这里得到了统一的解决,即RR和LL只是说其中节点的左或者右孩子为NULL,代码流程从本质上没有离开左旋或者右旋操作,所以不必要单独讨论额外进行操作
右旋:节点x失衡,对x进行右旋调整(左>右,指该失衡节点左右层数)
-
y = x->left
-
x->left = y->right
-
y->right = x
-
return y
针对理解这些操作最好的方法就是自己画一个因增加节点失去平衡的AVL树进行几次最小平衡树和左旋右旋的操作使其平衡
下面可以继续研究一下因插入导致失衡的四种情况:
假设失衡节点为x
-
LL:往x左孩子的左子树进行插入,导致x失衡:对x进行右旋(因为左>右)
-
RR:往x右孩子的右子树进行插入,导致x失衡:对x进行左旋(因为右>左)
-
LR:往x左孩子的右子树进行插入,导致x失衡:先对x->left左旋(x->left并不失衡,但是目的是为了将其处理为LL形,让插在右子树的新节点轮转到左子树),再对x右旋(x->left左旋,x的左>右是一开始就出现的问题)
-
RL:往x右孩子的左子树进行插入,导致x失衡:先对x->right右旋(x->right并不失衡,但是目的是为了将其处理为RR形,让插在左子树的新节点轮转到右子树),再对x左旋(x->left右旋,但x的右>左是一开始就出现的问题)
所以综上我们看到,左右旋的操作不止是将平衡二叉树搞平衡的一种操作,还可以对新插入节点位置进行左右子树的调换
但如果我们对LR和RL的情况进行直接右旋和左旋的操作会出问题吗?
也不会,只是我们为了更好的区分四种情况,才对每一种都进行单独讨论
代码部分
我们开始写代码
设置平衡二叉树的节点存储结构:
//设置节点
typedef struct node{
int data;
struct node *left;
struct node *right;
int h;//节点所处树的高度
}avlNode,*avlTree;
中序遍历输出:
//中序遍历-用于输出平衡二叉树的序列
void in_order(avlTree tree){
if(tree!=NULL){
in_order(tree->left);
printf("%d ",tree->data);
in_order(tree->right);
}
}
创建节点的函数,用于插入前的数据传输:
//创建新节点,该函数用于在插入函数中,即找到节点时使用
avlNode *create_node(int key,avlNode *left,avlNode *right){
avlNode *node = (avlNode*)malloc(sizeof(avlNode));
node->data = key;
node->left = left;
node->right = right;
node->h = 0;
return node;
}
获取节点高度:
//返回该节点处于树中的高度
int get_h(avlNode* node){
if(node==NULL){
return 0;
}
else{
return node->h;
}
}
四个旋转函数:
//四个函数对应四种情况
//单右旋LL
avlTree ll_rotation(avlNode *x){
//x为失衡节点,因在其左子树左孩子处插入
avlNode *y = x->left;
x->left = y->right;
y->right = x;
//改变x和y节点的高度
x->h = max(get_h(x->left),get_h(x->right)) + 1;
y->h = max(get_h(y->left),get_h(y->right)) + 1;
//除更改节点xy外其他节点的相对高度均没有发生变化
return y;
}
//单左旋RR
avlTree rr_rotation(avlNode *x){
//x为失衡节点,因在其右子树右孩子处插入
avlNode *y = x->right;
x->right = y->left;
y->left = x;
x->h = max(get_h(x->left),get_h(x->right)) + 1;
y->h = max(get_h(y->left),get_h(y->right)) + 1;
//除更改节点xy外其他节点的相对高度均没有发生变化
return y;
}
//双旋LR
avlTree lr_rotation(avlNode *x){
//失衡节点是由于 其左子树的右子树插入节点 所以先对x的左子树本身进行节点转移rr_rotation(x->left)
x->left = rr_rotation(x->left);
//此时以x节点为根节点所在树的结构变成了LL形
x = ll_rotation(x);
return x;
}
//双旋RL
avlTree rl_rotation(avlNode *x){
//失衡节点是由于 其右子树的左子树插入节点 所以先对x的右子树本身进行节点转移rr_rotation(x->left)
x->right = ll_rotation(x->right);
//此时以x节点为根节点所在树的结构变成了RR形
x = rr_rotation(x);
return x;
}
插入函数:
avlTree avlTree_insertNode(avlTree tree,int k){
//该递归插入巧妙地解决了新节点的插入 平衡因子的判断和树高度的值的返回
if(tree==NULL){
tree = create_node(k,NULL,NULL);
}
else if(k < tree->data){
tree->left = avlTree_insertNode(tree->left,k);//执行完插入
//判断平衡因子,该节点的左子树比右子树高,所以是L_
if(get_h(tree->left)-get_h(tree->right) >1){
//判断该节点左子树值与k值的关系以此判断是LL还是LR
if(k < tree->left->data){
//如果k值比较小,那么说明在x左孩子的左子树上
//LL
tree = ll_rotation(tree);
}
else{
//如果k值比较大,那么说明在x左孩子的右子树上
//LR
tree = lr_rotation(tree);
}
}
}
else{
tree->right = avlTree_insertNode(tree->right,k);//执行完插入
//判断平衡因子,该节点的右子树比左子树高,所以是R_
if(get_h(tree->right)-get_h(tree->left) >1){
//判断该节点左子树值与k值的关系以此判断是RR还是RL
if(k > tree->right->data){
//如果k值比较大,那么说明在x右孩子的右子树上
//RR
tree = rr_rotation(tree);
}
else{
//如果k值比较小,那么说明在x右孩子的左子树上
//RL
tree = rl_rotation(tree);
}
}
}
tree->h = max(get_h(tree->left),get_h(tree->right))+1;
return tree;
}
宏定义:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
#define max(a,b) ((a,b) ? (a) : (b))
main函数
int main()
{
int a[9];
srand(time(NULL));
for(int i=0;i<9;i++){
int random_num = rand()%100;
a[i] = random_num;
}
avlTree tree = NULL;
//int a[9] = {1,4,2,8,5,10,78,66,30};
for(int i=0;i<9;i++){
tree = avlTree_insertNode(tree,a[i]);
}
in_order(tree);
printf("\n");
return 0;
}
整体代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
#define max(a,b) ((a,b) ? (a) : (b))
//设置节点
typedef struct node{
int data;
struct node *left;
struct node *right;
int h;//节点所处树的高度
}avlNode,*avlTree;
//中序遍历-用于输出平衡二叉树的序列
void in_order(avlTree tree){
if(tree!=NULL){
in_order(tree->left);
printf("%d ",tree->data);
in_order(tree->right);
}
}
//创建新节点,该函数用于在插入函数中,即找到节点时使用
avlNode *create_node(int key,avlNode *left,avlNode *right){
avlNode *node = (avlNode*)malloc(sizeof(avlNode));
node->data = key;
node->left = left;
node->right = right;
node->h = 0;
return node;
}
//返回该节点处于树中的高度
int get_h(avlNode* node){
if(node==NULL){
return 0;
}
else{
return node->h;
}
}
//四个函数对应四种情况
//单右旋LL
avlTree ll_rotation(avlNode *x){
//x为失衡节点,因在其左子树左孩子处插入
avlNode *y = x->left;
x->left = y->right;
y->right = x;
//改变x和y节点的高度
x->h = max(get_h(x->left),get_h(x->right)) + 1;
y->h = max(get_h(y->left),get_h(y->right)) + 1;
//除更改节点xy外其他节点的相对高度均没有发生变化
return y;
}
//单左旋RR
avlTree rr_rotation(avlNode *x){
//x为失衡节点,因在其右子树右孩子处插入
avlNode *y = x->right;
x->right = y->left;
y->left = x;
x->h = max(get_h(x->left),get_h(x->right)) + 1;
y->h = max(get_h(y->left),get_h(y->right)) + 1;
//除更改节点xy外其他节点的相对高度均没有发生变化
return y;
}
//双旋LR
avlTree lr_rotation(avlNode *x){
//失衡节点是由于 其左子树的右子树插入节点 所以先对x的左子树本身进行节点转移rr_rotation(x->left)
x->left = rr_rotation(x->left);
//此时以x节点为根节点所在树的结构变成了LL形
x = ll_rotation(x);
return x;
}
//双旋RL
avlTree rl_rotation(avlNode *x){
//失衡节点是由于 其右子树的左子树插入节点 所以先对x的右子树本身进行节点转移rr_rotation(x->left)
x->right = ll_rotation(x->right);
//此时以x节点为根节点所在树的结构变成了RR形
x = rr_rotation(x);
return x;
}
avlTree avlTree_insertNode(avlTree tree,int k){
//该递归插入巧妙地解决了新节点的插入 平衡因子的判断和树高度的值的返回
if(tree==NULL){
tree = create_node(k,NULL,NULL);
}
else if(k < tree->data){
tree->left = avlTree_insertNode(tree->left,k);//执行完插入
//判断平衡因子,该节点的左子树比右子树高,所以是L_
if(get_h(tree->left)-get_h(tree->right) >1){
//判断该节点左子树值与k值的关系以此判断是LL还是LR
if(k < tree->left->data){
//如果k值比较小,那么说明在x左孩子的左子树上
//LL
tree = ll_rotation(tree);
}
else{
//如果k值比较大,那么说明在x左孩子的右子树上
//LR
tree = lr_rotation(tree);
}
}
}
else{
tree->right = avlTree_insertNode(tree->right,k);//执行完插入
//判断平衡因子,该节点的右子树比左子树高,所以是R_
if(get_h(tree->right)-get_h(tree->left) >1){
//判断该节点左子树值与k值的关系以此判断是RR还是RL
if(k > tree->right->data){
//如果k值比较大,那么说明在x右孩子的右子树上
//RR
tree = rr_rotation(tree);
}
else{
//如果k值比较小,那么说明在x右孩子的左子树上
//RL
tree = rl_rotation(tree);
}
}
}
tree->h = max(get_h(tree->left),get_h(tree->right))+1;
return tree;
}
int main()
{
int a[9];
srand(time(NULL));
for(int i=0;i<9;i++){
int random_num = rand()%100;
a[i] = random_num;
}
avlTree tree = NULL;
//int a[9] = {1,4,2,8,5,10,78,66,30};
for(int i=0;i<9;i++){
tree = avlTree_insertNode(tree,a[i]);
}
in_order(tree);
printf("\n");
return 0;
}
输出结果为:
自己看着办
结语
总的来说,平衡二叉树是较为繁琐的,需要自己进行理解,并记忆代码结构和思路。