💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
文章目录
前言
今天我将给大家介绍一篇有难度的知识点,相信大家在学校学习过数据结构的都知道,我们老师肯定说过AVL树,只是讲解怎么旋转的,而且就使用具体的树给大家演示了一下,也没有细讲,相信大家哪个时候是不理解的,今天这篇博客,带大家深入了解,AVL树到底是怎么旋转使他变得平衡,又是怎么控制平衡因子的,这个一会都会画图进行分类讨论的,大家也不要害怕,博主会用最生动的图带大家去理解的,话不多说我们开始进入正文的讲解。
一、AVL是什么?
在学习这棵树之前,我们肯定要知道这是一颗什么树,大家还记得博主之前写的二叉搜索树吗?之前的二叉搜索树他的时间复杂度不能够保持高度次,此次的AVL树就是解决这个问题的。
我们需要把高的以便进行旋转,把矮的一边往下面压使得左右高度差差不多,相信大家应该知道AVL树需要达到什么样的效果了吧。
接下来看看AVL的定义是什么:
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
1.它的左右子树都是AVL树
2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)=右子树的高度-左子树的高度
为什么不是左右高度差等于0呢,按道理左右高度差等于0才是绝对平衡啊,原因是树的结构,单结点偶数个就不可能做到左右高度差等于0,所以规定左右高度差不超过1.
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n),这也就是为什么要进行旋转的原因,可以提高性能。
与满二叉树的性能对比:
大家应该知道满二叉的二叉搜索树来说增删查改的时间复杂度都是O(
l
o
g
2
n
log_2 n
log2n),也就是高度次
所以说AVL树和满二叉树的性能差不多,所以这棵树也是我们必须要了解的,虽然以后工作不会让你手撕AVL树,但是原理要理解才能更好的使用。
博主只会带大家来实现插入操作,因为插入操作就可以把旋转的原理搞懂,其他的操作就是有其他的细节,而且AVL树其实也是为红黑树进行铺垫的。所以我们只需要搞懂AVL树的旋转原理即可。
二、AVL树的原理讲解
2.1AVL树节点的定义:
struct AVLTreeNode
{
T _val;
AVLTreeNode<T>* _left;
AVLTreeNode<T>* _right;
AVLTreeNode<T>* _parent;
int _bf;
AVLTreeNode(const T&val=T())
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
,_val(val)
{}
};
这里面我们采用三叉链,因为需要使用到父节点,所以使用三叉链找到父节点会非常的方便,但是在修改链接关系的时候就要注意别忘记修改此结点的父节点指向
2.2 AVL树的插入:
AVL树本质还是二叉搜索树,所以像之前一样找到插入位置进行插入,再来判断需不需要修改平衡因子,以及旋转,我们来看看代码:
bool insert(const T&val=T())
{
if (_root == nullptr)//空树的情况
{
_root = new Node(val);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)//找到插入位置
{
if (cur->_val < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_val > val)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(val);
if (parent->_val > val)//判断是属于父节点的哪一边
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;//修改父节点的链接关系
}
通过上面的代码,我们已经找到要插入的位置,看看插入新节点,会有哪些情况:
通过上面的图分析我们知道可以分成这五种情况,再新增之前肯定都是AVL树,新增之后只会影响从根到此结点路径上的结点,其他子树不hi受到影响,所以我们需要对这条路径上的所以结点都要进行他是不是一颗AVL树,所以我们使用循环更新的方式去实现。
来看代码:
这个代码是用来进行控制平衡因子和旋转的
while (parent)
{
if (parent->_left == cur)//新增再左.情况1
{
parent->_bf--;
}
else//新增在右.情况2
{
parent->_bf++;
}
if (parent->_bf == 0)//这是平衡因子为0,就不需要往上面进行更新了,结束.情况3
{
break;
}
else if (parent->_bf==1||parent->_bf==-1)//平衡因子为-1或者1,就不需要往上面继续更新了。情况4
{
//继续向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)//需要进行旋转,情况5
{
break;//这个为什么要break,原因是旋转之后的子树的根节点平衡因子一定变成了0,一会子啊介绍
}
else//这是为了防止出现平衡因子为3的情况,这样在插入之前的这棵树就不是平衡的因为插入只能+1或者-1.说明插入之前肯定存在右子树的高度差大于1
{
assert(false);
}
}
return true;//插入成功
}
其余的没什么好讲的,重点来讲解一下
2.3AVL的旋转
为什么要旋转,原因就是插入位置是左右高度差超过了1,所以导致不平衡,需要旋转,所以插入造成不平衡可以分为四种情况:
这上面的平衡因子就是我们后面选择那种旋转的条件
我们一个个的来分析,大家来看图解:
第一种情况:左单旋
上升到一般情况:
代码实现:
void RotateL(Node* parent)
{
Node* cur = parent->_right;
Node* pparent = parent->_parent;
Node* curleft = cur->_left;
parent->_right = cur->_left;//把parnet的右链接到cur的左
if(curleft)//cur的左边可能没有结点
curleft->_parent = parent;//curleft的父节点修改成parent
cur->_left = parent;//cur的左链接parent
parent->_parent = cur;//parent的父节点指向cur
if (parent == _root)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = cur;
}
else
{
pparent->_right = cur;
}
cur->_parent = pparent;
}
parent->_bf = cur->_bf = 0;//通过图分析,旋转之后parent和cur的平衡因子都变成了0
}
具体的解释都在图解里面,大家打开图解认真理解就可以了,对照代码去看就行了
第二种情况:右单旋
和第一种类似,就是换了方向:
上升到一般情况:
代码实现:
void RotateR(Node* parent)
{
Node* cur = parent->_left;
Node* pparent = parent->_parent;
Node* curright = cur->_right;
parent->_left = curright;
if (curright)//判断cur的右是否存在
curright->_parent = parent;
cur->_right = parent;
parent->_parent = cur;//修改三叉链
if (parent == _root)//parent是根节点的情况
{
_root=cur;
cur->_parent = nullptr;
}
else
{
if (pparent->_left ==parent)
{
pparent->_left = cur;//使用cur代替了parent的位置
}
else
{
pparent->_right = cur;
}
cur->_parent = pparent;
}
parent->_bf = cur->_bf = 0;
}
第三种和第四种情况:双旋
这两种几乎的分析思路一模一样,他两是在单旋的基础上进行旋转的,但是最重要分析的是平衡因子的变化,我们先来看看第三种情况的双旋:
这里也分析了平衡因子是怎么变化了。
来看代码:
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
RotateR(cur);
RotateL(parent);
if (bf == 0)//自己是新增结点
{
parent->_bf = 0;
cur->_bf = 0;
curleft->_bf = 0;
}
else if (bf == -1)//再curleft的左边插入
{
parent->_bf = 0;
cur->_bf = 1;
curleft->_bf = 0;
}
else if (bf == 1)//在curleft的右边插入
{
parent->_bf = -1;
cur->_bf = 0;
curleft->_bf = 0;
}
else
{
assert(false);
}
}
第四种情况我就没有画图了,和第三种一样,只要理解,靠想象就可以得出来平衡因子的变化
看代码:
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
RotateL(cur);
RotateR(parent);
if (bf == 0)//自己是新增结点
{
parent->_bf = 0;
cur->_bf = 0;
curright->_bf = 0;
}
else if (bf == -1)//在curright的右边插入
{
parent->_bf = 1;
cur->_bf = 0;
curright->_bf = 0;
}
else if (bf == 1)//在curright的左边插入
{
parent->_bf = 0;
cur->_bf = -1;
curright->_bf = 0;
}
else
{
assert(false);
}
}
上面已经介绍过四种旋转情况了,哪怎么知道什么时候使用那种旋转呢,每种旋转的父节点和父父节点的平衡因子都是固定了。通过一开始的四种情况插入图就可以得知。
旋转条件:
if (parent->_bf==2&&cur->_bf==1)
{
RotateL(parent);
//左单旋
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
//右单旋
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
//右左双旋
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
//左右双旋
}
else
{
assert(false);
}
通过上面图和代码大家应该之前说过的之前为什么要有break,因为旋转根节点的平衡因子都变成了0,不需要继续往上面进行更新了,所以退出循环就好了。
接下来就是测试AVL树是否符合要求
2.4AVL树的验证:
我们自己写一个函数,来自己计算每个结点高度,和平衡因子来对比,如果相同就代表正确,不相同就是不正确。
之前学过两叉树求高度的递归,这里就展开介绍了我们之间上代码:
bool isbalance()
{
return _isbalance(_root);
}
int height(Node* root)//求子树的高度
{
if (root == nullptr)
{
return 0;
}
int mleft = height(root->_left);
int nright = height(root->_right);
return mleft > nright ? mleft + 1 : nright + 1;
}
bool _isbalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int mleft = height(root->_left);
int nright = height(root->_right);
if (nright - mleft != root->_bf)//判断是否和平衡因子求出来的一样的。
{
cout << "平衡因子异常:" << root->_val<< "->" << root->_bf << endl;
return false;
}
return abs(mleft - nright) <= 1 && _isbalance(root->_left) && _isbalance(root->_right);
}
主函数:
int main()
{
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
AVLTree<int> av;
for (auto e : a)
{
av.insert(e);
cout << "Insert:" << e << "->" << av.isbalance() << endl;
}
return 0;
}
来看运行结果:
大家也可以自己下去把这颗树画出来看看对不对
2.5AVL树的删除
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不
错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
具体实现学生们可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。
2.6 AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这
样可以保证查询时高效的时间复杂度,即
l
o
g
2
(
N
)
log_2 (N)
log2(N)。但是如果要对AVL树做一些结构修改的操
作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,
有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数
据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
三、完整代码:
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
template<class T>
struct AVLTreeNode
{
T _val;
AVLTreeNode<T>* _left;
AVLTreeNode<T>* _right;
AVLTreeNode<T>* _parent;
int _bf;
AVLTreeNode(const T&val=T())
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
,_val(val)
{}
};
template<class T>
class AVLTree
{
typedef AVLTreeNode<T> Node;
public:
AVLTree(){}
bool insert(const T&val=T())
{
if (_root == nullptr)
{
_root = new Node(val);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_val < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_val > val)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(val);
if (parent->_val > val)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//控制平衡因子和旋转
//1.新增再左,父结点的平衡因子--
//2.新增再右,父结点的平衡因子++
//3.新增后,父节点的平衡因子变成了0则肯定是把矮的部分补充起来了,此时的父节点的父节点高度不会发生变化,就是不会再往上影响祖先了。不需要再往上面更新进行下一步的旋转
//4.新增后,父节点的平衡因子变成了-1或者1,就说明之前肯定的平衡因子是0,现在导致高度变化,则会影响祖先,所以需要一步步往上面进行更新
//5.新增后,父节点的平衡因子变成了-2或者2,就说明父节点这颗子树出现了不平衡,需要进行旋转,旋转会出现四种大的情况
//6.更新到根节点也会结束
while (parent)
{
if (parent->_left == cur)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
if (parent->_bf == 0)
{
break;
}
else if (parent->_bf==1||parent->_bf==-1)
{
//继续向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
if (parent->_bf==2&&cur->_bf==1)
{
RotateL(parent);
//左单旋
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
//右单旋
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
//右左双旋
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
//左右双旋
}
else
{
assert(false);
}
break;
}
else
{
assert(false);
}
}
return true;
}
bool isbalance()
{
return _isbalance(_root);
}
int height(Node* root)
{
if (root == nullptr)
{
return 0;
}
int mleft = height(root->_left);
int nright = height(root->_right);
return mleft > nright ? mleft + 1 : nright + 1;
}
bool _isbalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int mleft = height(root->_left);
int nright = height(root->_right);
if (nright - mleft != root->_bf)
{
cout << "平衡因子异常:" << root->_val<< "->" << root->_bf << endl;
return false;
}
return abs(mleft - nright) <= 1 && _isbalance(root->_left) && _isbalance(root->_right);
}
private:
void RotateL(Node* parent)
{
Node* cur = parent->_right;
Node* pparent = parent->_parent;
Node* curleft = cur->_left;
parent->_right = cur->_left;//把parnet的右链接到cur的左
if(curleft)//cur的左边可能没有结点
curleft->_parent = parent;//curleft的父节点修改成parent
cur->_left = parent;//cur的左链接parent
parent->_parent = cur;//parent的父节点指向cur
if (parent == _root)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = cur;
}
else
{
pparent->_right = cur;
}
cur->_parent = pparent;
}
parent->_bf = cur->_bf = 0;//通过图分析,旋转之后parent和cur的平衡因子都变成了0
}
void RotateR(Node* parent)
{
Node* cur = parent->_left;
Node* pparent = parent->_parent;
Node* curright = cur->_right;
parent->_left = curright;
if (curright)//判断cur的右是否存在
curright->_parent = parent;
cur->_right = parent;
parent->_parent = cur;//修改三叉链
if (parent == _root)//parent是根节点的情况
{
_root=cur;
cur->_parent = nullptr;
}
else
{
if (pparent->_left ==parent)
{
pparent->_left = cur;//使用cur代替了parent的位置
}
else
{
pparent->_right = cur;
}
cur->_parent = pparent;
}
parent->_bf = cur->_bf = 0;
}
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
RotateR(cur);
RotateL(parent);
if (bf == 0)//自己是新增结点
{
parent->_bf = 0;
cur->_bf = 0;
curleft->_bf = 0;
}
else if (bf == -1)//再curleft的左边插入
{
parent->_bf = 0;
cur->_bf = 1;
curleft->_bf = 0;
}
else if (bf == 1)//在curleft的右边插入
{
parent->_bf = -1;
cur->_bf = 0;
curleft->_bf = 0;
}
else
{
assert(false);
}
}
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
RotateL(cur);
RotateR(parent);
if (bf == 0)//自己是新增结点
{
parent->_bf = 0;
cur->_bf = 0;
curright->_bf = 0;
}
else if (bf == -1)//在curright的右边插入
{
parent->_bf = 1;
cur->_bf = 0;
curright->_bf = 0;
}
else if (bf == 1)//在curright的左边插入
{
parent->_bf = 0;
cur->_bf = -1;
curright->_bf = 0;
}
else
{
assert(false);
}
}
private:
Node* _root;
};
四、总结
我们讲完AVL树的基本原理了,但是这种只适合不修改结构,所以接下来我将讲解一种比较好的结构,红黑树,这个结构性能好,而且操作简单,也是为了set和map的模拟实现做铺垫,我们下篇再见