请回答数据结构【AVLTree】
0. Intro
map/multimap/set/multiset,在其文档介绍中发现,这几个容器有个共同点是:
其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接
近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
1. 高度平衡搜索二叉树
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当
于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1,即可降低树的高度,从而减少平均搜索长度。如果在任何时候它们的差异超过一个,则会进行重新平衡以恢复此属性。在平均情况和最坏情况下,查找、插入和删除都需要
O
(
l
o
g
2
N
)
O(log_2N)
O(log2N)
时间,是操作前树中的节点数。插入和删除可能需要通过一个或多个树旋转来重新平衡树。
From wikipedia
1.1 平衡因子
AVL树中,平衡因子不是必须的,只不过是因为这样更易于理解
节点的平衡因子是左右子树的高度差。也就是右子树减左子树
AVL 树中每个节点的平衡因子应该是**+1**、0或**-1**。
From https://www.educative.io/edpresso/common-avl-rotation-techniques
2. AVL树简单实现
这里只写几个最简单的接口,而没有封装迭代器和深拷贝什么的,因为红黑树用的更多一点,而这里和红黑树有些地方还是很像的,如果关注这些其他的接口的话,大家可以关注一下之后的红黑树博客
2.1 AVL树节点结构
AVL树我们这里采用的是三叉链结构同时有一个平衡因子,三叉链的原因后面会讲
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; // 平衡因子 balance factor
pair<K, V> _kv;
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
, _kv(kv)
{}
};
可以看到AVL树的开销还是较大的,因为每存一个KV型参数那么就需要右16个字节的额外空间代价,不过只能说这个待机是值得的
2.2 Insert
From https://commons.wikimedia.org/wiki/File:AVL_Tree_Example.gif
2.2.1 搜索二叉树部分
虽然是AVL是三叉链结构,有一个parent指针,但是这不代表我们就不需要再插入的时候写一个parent记录父亲节点的位置了,因为
我们先完成二叉搜索树的功能,这和之前的二叉搜索树很像
bool Insert(const pair<K, V>& kv)
{//1. 空树
if (_root==nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = _root, * cur = _root;
//2. 找到节点
while (cur)
{//普通版本不允许冗余
if (cur->_kv.first>kv.first)
{
parent = cur;
cur = cur->_left;
}
else if(cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else//数据重复直接false
return false;
}
cur = new Node(kv);
//连接节点,注意是三叉链结构
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
return true;
}
2.2.2 平衡部分
In case a node is imbalanced, a rotation technique can be applied to balance it.
然后要完成AVL树的平衡功能,AVL树可以通过旋转来保持平衡,也就是说AVL树的关键是控制平衡
那么下面为了要旋转,我们要去更新平衡因子,首先我们要知道插入的节点有三种可能情况。同时我们要知道我们插入之前是一棵AVL树,插入之后还要是一棵AVL树,那就需要先更新平衡因子,然后旋转操作
2.2.3 更新平衡因子
这里来思考一下,插入对于平衡因子的影响(平衡因子是否需要更新)取决于:左右子树的高度是否变化
所以说凡是插入一个节点的话,这个节点的祖先都是可能会受到影响的,那么我们的更新的过程就是从当前插入节点去挨个倒着修改祖先的过程,那么这也就是为什么我们需要一个三叉链结构,指向父亲的指针(当然不是说平衡树都要三叉链,还可以借助栈结构,但是不管怎么说都是三叉链简单一点)
Situation | 更新平衡因子(平衡因子是右树减左树) | 说明 |
---|---|---|
新节点是左孩子 | parent->bf– | 左边多一个高度 |
新节点是右孩子 | parent->bf++ | 有百年多一个高度 |
父亲的平衡因子是1 or -1 | 继续往上更新 | 父亲所在的子树的高度变了,从0变来的 |
父亲的平衡因子是 0 | 停止更新 | 不要往上了,已经OK了 |
父亲的平衡因子是2 or -2 | 旋转处理 | 已经不平衡,要处理了 |
//4. 更新平衡因子
while (cur != _root)
{
if (parent->_left == cur)
cur->_parent->_bf--;//新节点是在parent左
else
cur->_parent->_bf++;//新节点是在parent右
//bf==0 停止
if (parent->_bf == 0)
break;
//bf==1 || bf==-1 继续往上走
else if (parent->_bf==1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}
//旋转
else if(parent->_bf == 2 || parent->_bf == -2)
{
//parent所在的子树不平衡,需要旋转处理一下
}
else//之前的出错了,暴力assert
assert(false);
}
2.2.4 完整插入(旋转在后面)
//插入
pair<Node*,bool> Insert(const pair<K, V>& kv)
{
//1. 空树
if (_root==nullptr)
{
_root = new Node(kv);
return make_pair(_root, true);
}
Node* parent = _root, * cur = _root;
//2. 找到节点
while (cur)
{//普通版本不允许冗余
if (cur->_kv.first>kv.first)
{
parent = cur;
cur = cur->_left;
}
else if(cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else//数据重复直接false
return make_pair(cur,false);//插入失败返回和这个节点相等的节点
}
cur = new Node(kv);
Node* newnode=cur
//3. 连接节点,注意是三叉链结构
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
//4. 更新平衡因子
while (cur != _root)
{
if (parent->_left == cur)
cur->_parent->_bf--;//新节点是在parent左
else
cur->_parent->_bf++;//新节点是在parent右
//1. bf==0 停止
if (parent->_bf == 0)
{
break;
}
//2. bf==1 || bf==-1 继续往上走
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}
//3. _bf == 2 ||_bf == -2 parent所在的子树不平衡,需要旋转处理一下
else if (parent->_bf == 2 || parent->_bf == -2)
{
if (parent->_bf == -2)
{
if (cur->_bf == -1)
{//右单旋
_Rotate_R(parent);
}
else //cur->_bf == 1
{//左右双旋
_Rotate_LR(parent);
}
}
else //parent->_bf == 2
{
if (cur->_bf == 1)
{
//左单旋
_Rotate_L(parent);
}
else //cur->_bf == -1
{//右单旋
_Rotate_RL(parent);
}
}
break;
}
else//之前的出错了,暴力assert
{
assert(false);
}
}
return make_pair(newnode,true);
}
2.3 Rotation
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡
化。根据节点插入位置的不同,AVL树的旋转分为四种:
下面是不同场景的具象图:
- 右单旋
将节点插入左子树的左子树时应用的单次旋转。在给定的示例中,在插入节点 A之后,节点 C现在的平衡因子为 2 。通过向右旋转树,节点 B成为根,从而形成平衡树。
- 左单旋
当节点插入右子树的右子树时应用的单个旋转。在给定的示例中,节点 A在插入节点 C后的平衡因子为 2 。通过向左旋转树,节点 B成为根,从而形成平衡树。
- 左-右旋
双旋转,左旋转后右旋转。在给定的示例中,节点 B导致不平衡导致节点 C的平衡因子为 2。由于节点 B插入到节点 A的右子树中,因此需要应用左旋转。但是,单次旋转不会给我们所需的结果。现在,我们所要做的就是如前所示应用正确的旋转来实现平衡树。
- 右-左旋
双旋转,右旋转后左旋转。在给定的示例中,节点 B导致不平衡导致节点 A的平衡因子为 2。由于节点 B插入到节点 C的左子树中,因此需要应用右旋转。然而,和以前一样,单次旋转不会给我们所需的结果。现在,通过如前所示应用左旋转,我们可以实现平衡树。
下面是具象图gif分析:
From https://wkdtjsgur100.github.io/avl-tree/
2.3.1 Right Rotation
通过之前的文字和图就可以分析得到右单旋需要进行如下操作:(针对抽象图)
顺序 | 操作 |
---|---|
1. | β做B的左子树 |
2. | B做A的右子树 |
3. | A变成了根 |
4. | 修改平衡因子 |
旋转代码落实之前,先厘清好相对关系,看图写代码
然后开始写代码,要注意subLR可能是空,还要注意修改平衡因子
void _Rotate_right(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
//subLR有可能是空的
if (subLR)
{
subLR->_parent = parent;
}
subL->_right = parent;
parent->_parent = subL;
subL->_bf = parent->_bf = 0;
}
右旋还存在着问题,因为这棵树只不过是一颗子树罢了,光旋转子树根本不够对吧,所以我们还要搞定当前根节点的父亲儿子关系,也就是找到该节点原来爷爷节点然后连接上才可以,很细节
void _Rotate_R(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//1. 旋转
parent->_left = subLR;
//subLR有可能是空的
if (subLR)
{
subLR->_parent = parent;
}
subL->_right = parent;
Node* grandparent = parent->_parent;//先记录parent->_parent
parent->_parent = subL;
//2. 修改新父子关系
if (parent == _root)
{//独立子树
_root = subL;
_root->_parent = nullptr;
}
else
{//还有父亲,那要修改父亲,就要记录父亲的父亲
if (grandparent->_left == parent)
grandparent->_left = subL;
else
grandparent->_right = subL;
subL->_parent = grandparent;
}
//3. 修改平衡因子
subL->_bf = parent->_bf = 0;
}
2.3.2 Left Rotation
顺序 | 操作 |
---|---|
1. | β做A的右子树 |
2. | A做B的左子树 |
3. | B变成了根 |
4. | 修改平衡因子 |
void _Rotate_L(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//1. 旋转
parent->_right = subRL;
if (subRL)
{
subRL->_parent = parent;
}
subR->_left = parent;
Node* grandparent = parent->_parent;
parent->_parent = subR;
//2. 修改新的父子关系
//空树
if (parent == _root)
{
_root = subR;
_root->_parent = nullptr;
}
else
{//还有父亲,那要修改父亲,就要记录父亲的父亲
if (grandparent->_left == parent)
grandparent->_left = subR;
else//grandparent->_right == parent
grandparent->_right = subR;
subR->_parent = grandparent;
}
//3. 修改bf
subR->_bf = parent->_bf = 0;
}
2.3.3 Left-Right Rotation
顺序 | 操作 |
---|---|
1. | 以3为轴左单旋 |
2. | 以4为轴右单旋 |
3. | 修改平衡因子 |
双旋转麻烦的点是在于平衡因子的修改而不是在于旋转,旋转只需调一下函数就可以了
我们需要看透这个双旋转的本质就是把4的左右孩子交给3和5,然后自己做根,此时的平衡因子也要分情况讨论
Situation | subLR->bf | 平衡因子修改 |
---|---|---|
情况一 | bf==-1 | 从左到右 0 0 1 |
情况二 | bf==-1 | 从左到右 -1 0 0 |
情况三 | bf==0 | 从左到右 0 0 0 |
void _Rotate_LR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
//先旋转左
_Rotate_L(parent->_left);
//再旋转右
_Rotate_R(parent);
//平衡因子调节
if (bf == -1)
{
subL->_bf = 0;
parent->_bf = 1;
subLR->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
2.3.4 Right-Left Rotation
顺序 | 操作 |
---|---|
1. | 以5为轴右单旋 |
2. | 以4为轴左单旋 |
3. | 修改平衡因子 |
修改平衡因子:
Situation | subRL->bf | 平衡因子修改 |
---|---|---|
情况一 | bf==-1 | 从左到右 -1 0 0 |
情况二 | bf==-1 | 从左到右 0 0 1 |
情况三 | bf==0 | 从左到右 0 0 0 |
void _Rotate_RL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
//先右旋
_Rotate_R(parent->_right);
//再左旋
_Rotate_L(parent);
// 平衡因子更新
if (bf == 1)
{
subR->_bf = 0;
parent->_bf = -1;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else
assert(false);
}
2.4 InOrder
下面为了验证我们的AVL树首先起码搜索功能得是对的,验证Insert的正确性,我们可以打印一遍中序
void _InOrder(Node *root)
{
if (root==nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
}
2.5 IsAVLTree
下面为了查一下写的AVLTree的Insert是不是插入后还是一个有平衡功能的搜索树,我们写一个函数来验证
这个方法就是通过算左右树的高度看看是否平衡
先是有一个判断平衡,判断平衡操作可以顺带检查一下平衡因子是不是有问题。同时也可以判断左右子树高度差,求高度只需要在封装一个函数既可以了
int _Height(Node* root)
{
if (root==nullptr)
{
return 0;
}
int left_height = _Height(root->_left);
int right_height = _Height(root->_right);
return left_height > right_height ? left_height + 1 : right_height + 1;
}
bool _IsBalance(Node* root)
{
if (root==nullptr)
{
return true;
}
int left_height = _Height(root->_left);
int right_height = _Height(root->_right);
//检查平衡
if (right_height - left_height != root->_bf)
{
cout << "平衡因子异常:" << root->_kv.first << endl;
return false;
}
return abs(right_height - left_height) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
bool IsAVLTree()
{
return _IsBalance(_root);
}
2.6 Find
和搜索树一样
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
{
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
2.7 析构函数
void _Destroy(Node* root)
{
if (root==nullptr)
{
return;
}
_Destroy(root->_left);
_Destroy(root->_right);
delete root;
}
//析构
~AVLTree()
{
_Destroy(_root);
_root = nullptr;
}
2.8 operator[]
V& operator[](const K& key)
{
pair<Node*, bool> ret = Insert(make_pair(key, V()));
return ret.first->_kv.second;
}
2.9 Erase
删除部分只需了解即可
- 先找到相应节点,按照搜索树规则分类删除
- 按照搜索树规则分类删除
- 左为空
- 右为空
- 左右都不为空
- 更新平衡因子,如果出现不平衡->旋转
- 删除在parent左,parent->bf++
- 删除在parent右,parent->bf–
- 更新后
parent->bf==0
,说明原来是1或者-1,高度变了,继续往上更新 - 更新后
parent->bf==1 || parent->bf==1
,说明原来是0,现在删除一个, parent高度不变,不影响上一层,可以停止了 - 更新之后
parent->bf==2 || parent->bf==-2
不平衡要旋转
2.10 拷贝构造和赋值运算符
可以参考搜索二叉树,和之前的如出一辙
https://blog.csdn.net/Allen9012/article/details/124435568
3. AVL性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即
l
o
g
2
N
log_2N
log2N
但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:
插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
相关代码在我的github仓库https://github.com/Allen9012/cpp/tree/main/C%2B%2B%E8%BF%9B%E9%98%B6/AVLTree