注:本篇的介绍风格会偏笔记向,如果第一次接触AVL树实现,看完本文关于旋转实现的思想还未完全理解,可以看看下面两个我比较推荐的教程,也作为作者本文的参考博客。
1. 手把手教AVL树(图文解析向博客)
2. AVL树从入门到入土(B站视频 “看完还不懂可以选择弃疗” )
下面步入正题:
AVL树基本概念
- 中文名称:平衡二叉树
- 结构特点:树中的任意节点的左右子树的高度之差不超过1的二叉搜索树
- 实现核心操作:查询、插入、删除、旋转
- 时间复杂度:查询、插入、删除的时间复杂度均为O(logn)
AVL模块化实现
结构体声明
struct AVLNode{
int value;
int balance;
AVLNode* left;
AVLNode* right;
AVLNode* parent;
};
struct AVLTree{
AVLNode* root;
}tree;
- 平衡因子BF(Balanced Factor):该结点的左子树与右子树的高度之差的值。在每个节点中都要附有这样一个标签。
- 平衡的识别条件:根据AVL的定义,树中的任意节点的左右子树的高度之差不超过 1。所以,AVL树中的任意结点的平衡因子只可能是:
① -1(右子树高于左子树)
② 0 (左右子树高度相等)
③ 1 (左子树高于右子树)
如果因为插入新的节点或删除某一个节点使某个其他节点的BF值变为-2或2,则视为当前状态为不平衡的状态,那么就要通过旋转操作来使其恢复平衡。
那么接下来获取高度的方法和二叉搜索树是一样的。
二叉排序树基本操作
递归查询即将要插入/删除的节点位置
struct AVLNode* select(int value,AVLNode* root){
if(root==0){
return 0;
}
if(root->value==value){
return root;
}
if(root->value>value){
if(root->left)
return select(value,root->left);
else
return root;
}
if(root->value<value){
if(root->right)
return select(value,root->right);
else
return root;
}
}
select函数要点:
- 这段代码select函数将插入和删除功能中要进行的递归查询操作封装在了一起
- 考虑空树情况,返回值为0
- 如果value参数值与查询节点的value值相等,返回该节点。这部操作是为删除操作服务的。
- 如果value参数值比查询节点值大/小,递归得到该节点的右/左节点的select函数返回值,如果被查询子节点为NULL,则返回该节点,在insert函数中生成新的子节点。
插入新节点
void insert(int value,AVLNode* root){
AVLNode* node=select(value,root); // 找到即将被插入的目标节点
if(node==0){ // 如果树为空
tree.root=(AVLNode*)malloc(sizeof(AVLNode));
tree.root->value=value;
tree.root->left=tree.root->right=0;
tree.root->parent=0;
tree.root->balance=0;
}
else if(node->value!=value){
if(node->value>value){
node->left=(AVLNode*)malloc(sizeof(AVLNode));
node->left->value=value;
node->left->left=node->left->right=0;
node->left->parent=node;
node->left->balance=0;
rebalance(node);
}else if(node->value<value){
node->right=(AVLNode*)malloc(sizeof(AVLNode));
node->right->value=value;
node->right->left=node->right->right=0;
node->right->parent=node;
node->right->balance=0;
rebalance(node);
}
}
}
Insert函数要点:
- 考虑空树情况
- 通过递归比较树中的节点并插入(这里通过select函数找到被插入节点后再insert函数体内再次进行左右树判断之所以略显繁琐,是为了方便删除节点时复用select函数体内逻辑。)
- [AVL树] 检测并恢复平衡:rebalance函数。
删除某一节点
void delete(int value,AVLNode* root){
AVLNode* node=select(value,root);
if(node->value==value){
if(node->left&&node->right){
delete_Twochild(node);
}else{
delete_NotTwochild(node);
}
}
}
delete函数要点:
- 通过select函数递归查找被删除节点,节省效率。
- 考虑被删除节点子树个数的情况:
- 有两个子树
- 有一个或没有子树
被删除节点有一个子树或没有子树的情况
void delete_NotTwochild(AVLNode* node){
if(node->parent==0){
if(node->left){
tree.root=node->left;
node->left->parent=0;
}else{
tree.root=node->right;
node->right->parent=0;
}
}else{
if(node->parent->left==node){
if(node->left){
node->parent->left=node->left;
node->left->parent=node->parent;
}else{
node->parent->left=node->right;
if(node->right)
node->right->parent=node->parent;
}
}else{
if(node->left){
node->parent->right=node->left;
node->left->parent=node->parent;
}else{
node->parent->right=node->right;
if(node->right)
node->right->parent=node->parent;
}
}
rebalance(node->parent);
}
}
delete_NotTwochild函数要点:
- 如果删除节点子节点数量为0或1,执行该函数。
- 要考虑到被删除节点是根节点的情况,因为要避免root->parent为空指针的情况。
- 通过node->parent->left == node的方式判断该节点是父节点的左孩子还是右孩子。
- 先判断出该节点是其父亲节点的左子树还是右子树,方便在删除操作后将目标删除节点的父亲节点的孩子指针指向目标删除节点的孩子节点。
- 然后判断目标删除节点的孩子在左子树还是右子树还是不存在。
- 不管哪种情况,将目标删除节点的父亲节点的孩子指针指向目标删除节点的孩子节点。
- 下一步要将目标删除节点的孩子节点的父亲指针指向删除节点的父亲节点,考虑到目标删除节点没有孩子的情况会导致node->left->parent出现空指针情况。所以要在这部之前先判断删除节点是否有孩子。
- [AVL树]完成二叉排序树的基本删除操作后,检查并恢复平衡,rebalance函数。
void delete_Twochild(AVLNode* node){
AVLNode* after=getMin(node->right);
node->value=after->value;
delete_NotTwochild(after);
}
delete_Twochild函数要点:
- 如果删除节点有两个子节点,执行该函数。
- 这种情况只需要在删除节点的右子树中通过getMin函数寻找到value值最小的节点,将删除节点的值与其互换,再将找到的最小value值的节点通过delete_NotTwochild函数删除。
struct AVLNode* getMin(AVLNode* node){
if(node->left)
getMin(node->left);
else
return node;
}
getMin函数要点:
- 此函数目的在于寻找某一节点作为根节点的树中value值最小的节点。
- 根据二叉排序树,递归查找最左侧节点即可。
至此,二叉排序树基本操作介绍完成。
AVL树旋转操作
获取高度
int height(AVLNode* a){
if(a == 0){
return 0;
}
int rightheight = height(a->right);
int leftheight = height(a->left);
return rightheight > leftheight ? (rightheight+1) : (leftheight+1);
}
height函数要点:
- 考虑空树情况。
- 分别递归查询左右子树高度,层层递归向上返回高度累加值。
检查并回复平衡
void rebalance(AVLNode* a){
setBalance(a);
if(a->balance== -2){
if(a->left->balance <=0){
a=turnRight(a);
}else{
a=turnLeftThenRight(a);
}
}else if(a->balance==2){
if(a->right->balance>=0){
a=turnLeft(a);
}else{
a=turnRightThenLeft(a);
}
}
if(a->parent){
rebalance(a->parent);
}else{
tree.root=a;
}
}
- 首先通过setBalance函数刷新被检查节点的balance值。
- 开始检查节点balance值:分为四种不平衡情况,对应以下四种旋转方案。
- 递归向父亲节点递归检查,直到查到根节点。
刷新节点balance值
void setBalance(AVLNode* a){
if(a)
a->balance=height(a->right)-height(a->left);
}
单次旋转(向左/右旋转)
struct AVLNode* turnLeft(AVLNode* a){
AVLNode* b=a->right;
if(a->parent!=0){
if(a->parent->right==a){
a->parent->right=b;
}else{
a->parent->left=b;
}
}
b->parent=a->parent;
a->parent=b;
a->right=b->left;
b->left=a;
if(a->right!=0)
a->right->parent=a;
setBalance(a);
setBalance(b);
return b;
}
struct AVLNode* turnRight(AVLNode* a){
AVLNode* b=a->left;
if(a->parent!=0){
if(a->parent->right==a){
a->parent->right=b;
}else{
a->parent->left=b;
}
}
b->parent=a->parent;
a->left=b->right;
if(a->left!=0)
a->left->parent=a;
a->parent=b;
b->right=a;
setBalance(a);
setBalance(b);
return b;
}
单次旋转函数要点:
- 单次旋转函数触发条件:
Turn Right: node->balance = -2 && node->left->balance <= 0
Turn Left:node->balance = 2 && node->right->balance >= 0 - 具体处理步骤:(Turn Right 举例)
① 把a节点以及他的右子树移除,a的左子节点b代替a的位置
if(a->parent!=0){
if(a->parent->right==a){
a->parent->right=b;
}else{
a->parent->left=b;
}
}
②把b节点的右子树移动成为a节点的左子树
a->left=b->right;
if(a->left!=0)
a->left->parent=a;
③让a节点成为b节点的右孩子
a->parent=b;
b->right=a;
④刷新a,b节点的balance值。
⑤return b节点,让rebalance函数重新从b节点向上检查新树的平衡性。
两次旋转
struct AVLNode* turnLeftThenRight(AVLNode* a){
a->left = turnLeft(a->left);
return turnRight(a);
}
struct AVLNode* turnRightThenLeft(AVLNode* a){
a->right = turnRight(a->right);
return turnLeft(a);
}
两次旋转函数要点:
- 两次旋转函数触发条件:
Turn Right Then Left: node->balance = 2 && node->left->balance = -1
Turn Left Then Right:node->balance = -2 && node->right->balance = 1 - 核心实现思路:对不平衡节点的不平衡方向的子节点先做旋转操作,再对不平衡节点做第二次旋转。
至此,旋转操作介绍完毕。