说在前面的话:
🍁🍁室友说面试了AVL树的旋转,我想了一下得赶紧搞篇博客巩固巩固
🍁🍁本篇博客重点搞AVL树的旋转
AVL树的概念
前面提到的二叉搜索树虽然提高了查找的效率,但是一旦数据有序或者接近有序那搜索树就会退化成单支,查找元素相当于在顺序表中搜索元素,效率很低。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
注意:****一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 ,O(log2 N)搜索时间复杂度O( log2N)。
AVL树结点的定义
室友建议我用三叉链实现,不用三叉链很麻烦,还需要借助栈比较难搞。博主就搞成三叉链,并引入平衡因子(平衡因子=右子树的高度-左子树的高度)室友还叫我写成KV模型的,那博主还是听从室友的建议。此外还要提供一个构造函数来构造新的结点,构造出来的结点的左右子树为空,将构造的新节点的平衡因子初始化成0即可。
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;//该结点的左孩子
AVLTreeNode<K, V>* _right;//该结点的右孩子
AVLTreeNode<K, V>* _parent;//该结点的双亲
pair<K, V> _kv;
int _bf;//该结点的平衡因子
AVLTreeNode(const pair<K,V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0)
{}
};
AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为3步:
1.按照二叉搜索树的方式插入新节点
2.调整节点的平衡因子
3.如果出现不平衡则需要进行旋转处理
由于AVL树本身就是二叉搜索树,插入还是和搜索树是一样的
1.当插入结点的key值比当前结点的key值小,则插入到该结点的左子树
2.当插入结点的key值比当前结点的key值大,则插入到该结点的右子树
3.当插入结点的key值等于当前结点的key值,则插入失败
一个结点的平衡因子是否需要更新,取决于该结点的左右子树的高度是否发生变化。当插入一个新的结点它祖先的平衡因子都会收到影响。
平衡因子的更新规则:
1.新增结点在parent的右边,平衡因子++
2.新增结点在parent的左边,平衡因子- -
每更新完一个结点的平衡因子后,都需要进行判断:
1.parent的平衡因子等于1或者-1则继续往上更新
2.parent的平衡因子等于0,则停止更新
3.parent的平衡因子等于2或者-2,说明已经不平衡了需要进行旋转处理
分析:
1.parent更新后的平衡因子为1或-1,则说明在parent的右边或者左边新增了结点,从而影响了parent的父亲结点所以要继续往上更新平衡因子。
2.parent更新后的平衡因子为0,1或-1经过++或- -变成0,说明新增结点在parent高的一侧使得parent的左右子树一样高,不会影响parent的父亲结点,就不用往上更新平衡因子。
3.parent更新后的平衡因子为2或-2,parent的左右子树差的绝对值大于1,已经不满足AVL树,需要旋转处理。
最坏的情况是调整到根节点,例如下面的情况:
假设以parent为根的子树不平衡,那么parent的平衡因子为2或者-2,分为下面的情况考虑:
- parent 的平衡因子为2,说明parent的右子树高,设parent的右子树的根subR
当subR的平衡因子为1时,说明新增结点在subR的右边,需要进行左单旋
当subR的平衡因子为1时,说明新增结点在subR的左边,需要进行右左双旋
我问室友有没有好记的办法,他画了一张草图给我:
- parent 的平衡因子为2,说明parent的右子树高,设parent的右子树的根subL
当subL的平衡因子为-1时,进行右单旋
当subL的平衡因子为1时,进行左右双旋
我就说你画的真好下次就不要画了吧.具体的旋转细节往下看。
插入的代码如下:
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree()
:_root(nullptr)
{}
bool insert(const pair<K, V>& kv)
{ //树为空,new一个新节点
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (cur->_kv.first > kv.first)//插入结点的key值小于当前结点,往左走
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)//插入结点的key值大于当前结点,往右走
{
parent = cur;
cur = cur->_right;
}
else
{
return false;//没有找到,返回false
}
}
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
//注意是三叉链,还要链接上parent
parent->_right = cur;
cur->_parent = parent;
}
else
{
//要链接上parent
parent->_left = cur;
cur->_parent = parent;
}
//控制平衡
//1.更新平衡因子
//2.如果出现不平衡则需要旋转处理
while (parent)
{
//在父亲结点左边插入平衡因子--,右边则++
if (parent->_left == cur)
parent->_bf--;
else
parent->_bf++;
//1.平衡因子=0就不要更新平衡因子了
//2.=1或-1则要继续往上调整
//3.=2或-2则要旋转处理
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)
{
if (cur->_bf == -1)
{
//左边高,右单旋处理
RotateR(parent);
}
else
{
//左右双旋
RotateLR(parent);
}
}
else //parent->_bf == 2
{
if (cur->_bf == 1)
{
//右边高,左单旋
RotateL(parent);
}
else
{
//右左双旋
RotateRL(parent);
}
}
break;
}
else
{
//走到这里就不是旋转的问题,你要检查别的问题
assert(false);
}
}
return true;
}
private:
Node* _root;
};
这是插入的整体大逻辑,接下来就要一个一个的实现旋转了。首先得实现左单旋和右单旋,双旋的话就可以调用单旋。
右单旋
具象图:
因为有太多种情况了所以要用抽象图来概括:
将30定义成parebt->_left,b设为shuLR,是subL的右孩子,这样定义的好处是好处理链接关系。
动图演示:
需要注意的细节:
1.subLR可能为空
2.先该30的右给60的左,在让60整棵树左30的右子树。因为是三叉链实现的,所以还要注意修改动的结点的父亲结点
3.60可能是子树,也有可能是单独的树
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parentParent = parent->_parent;//记录parent的父亲结点
//subLR做parent->_left
parent->_left = subLR;
subL->_right = parent;
//同时更新动的2个节点的parent
//注意subLR还可能是空结点
if (subLR)
subLR->_parent = parent;
parent->_parent = subL;
//parent可能是单独的树,或者子树,分情况
if (_root == parent)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
//还有可能parent是子树,可能是左子树
if (parentParent->_left == parent)
parentParent->_left = subL;
else
//也可能是右子树
parentParent->_right = subL;
//调整subL的父亲结点
subL->_parent = parentParent;
}
//调整平衡因子
parent->_bf = subL->_bf = 0;
}
室友说旋转的细节还是很多的,你要是不注意就会出错,就可能把握不住。
左单旋
有了右单旋的基础后再来搞左单旋的话,可能会轻松一点。还是跟上面的一样,先画具象图,在画抽象图概括所有的情况。
具象图:同样用右旋转的定义方法
抽象图
动图演示:
左单旋代码
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parentParent = parent->_parent;
//先旋转
parent->_right = subRL;
subR->_left = parent;
parent->_parent = subR;
//在改父亲结点
if (subRL)
subRL->_parent = parent;
if (_root == parent)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
//subR旋转后可能是左右子树2种情况
if(parentParent->_left == parent)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
//调整平衡因子
parent->_bf = subR->_bf = 0;
}
细节跟右单旋是一样的,这里就不多做解释了。
右左双旋
双旋的话就直接上抽象图了,具象图感兴趣的小伙伴可以自己动手画,只有自己动手画才可能把握的住。双旋先旋转parent的右子树,在将parent左单旋。旋转的逻辑是简单了,难把握的是平衡因子不好调节了。
平衡因子分为3种情况调节:
第1种情况:新增结点在c的右边:
第2种情况:新增结点在b的左边
第3种情况:结点60就是新增结点:
总结上面的情况:
需记录subRL的平衡因子,还需保存subR,subRL.
根据subRL的平衡因子在更新其他需要更新的平衡因子
1.当subRL的平衡因子=1时,subR,subRL,parent的平衡因子分别是0,0,-1
2.当subRL的平衡因子=-1时,subR,subRL,parent的平衡因子分别是1,0,0
3.当subRL的平衡因子=0时,subR,subRL,parent的平衡因子的都是0
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;//记录subRL的平衡因子
RotateR(parent->_right);
RotateL(parent);
// 平衡因子更新
//分3种情况
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);
}
}
图一定要画好,图画完一遍就是理解一遍旋转。代码是次要的。
左右双旋
跟上面的双旋是一样的。这里博主就直接画出三种情况,不做多余的讲解了。
第1种情况:b是新增结点
第2种情况:c是新增结点
第3种情况:60就是新增结点
跟上面的双旋的逻辑是一样的。
总结平衡因子的变化:
根据subLR的平衡因子在更新其他需要更新的平衡因子
1.当subLR的平衡因子=1时,subL,subLR,parent的平衡因子分别是-1,0.0;
2.当subLR的平衡因子=-1时,subL,subLRL,parent的平衡因子分别是0,0,1;
3.当subLR的平衡因子=0时,subL,subLR,parent的平衡因子的都是0
代码如下:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == -1)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else if (bf == 0)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else
assert(false);
}
AVL树的验证
到这里AVL树的旋转才算是结束了。那么接下来就是要测试自己的旋转有没有问题。
#include"AVLtree.h"
int main()
{
int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16,14};
//int arr{ 16, 3, 7, 11, 9, 26, 18, 14, 15 };
AVLTree<int, int> t;
for (auto e : arr)
{
t.insert(make_pair(e,e));
}
t.InOrder();
return 0;
}
用2组特殊的测试用例来测试一波
到这里只能说明是搜索树没有问题。我们还要增加一个能检测我们的树是否平衡。
1.先计算出左右子树的高度
2.再检测每课子树的平衡因子
bool IsAVLTree()
{
return _IsBalance(_root);
}
bool _IsBalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
// 检查一下平衡因子是否正确
if (rightHeight - leftHeight != root->_bf)
{
cout << "平衡因子异常:" << root->_kv.first << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
int _Height(Node* root)
{
if (root == nullptr)
{
return 0;
}
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return rightHeight > leftHeight ? rightHeight + 1 : leftHeight + 1;
}
在测试中加上IsAVLTree()检测是否是AVL树。
到这里AVL树的插入才算没问题。
一定要注意细节,一点细节的错误导致整个插入出错。因为博主因为小的失误调试了好几个小时。还要用好编译器的调试功能,能帮助我们很多。
AVL树的查找
AVL树的查找跟搜索树一样很简单
1.若为空树,返回空
2.key值比当前结点的key值大就到右边找
3.key值比当前结点的key值大就到左边找
4.key值和当前结点的key值相等或者没找到返回false
Node* Find(const K& key)
{
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;
}
AVL树的修改
修改可以直接调用insert,在上一篇的map中说到:
insert的返回值:
它的返回值是pair<iterator,bool>,第1个类型是迭代器,第2个类型是bool类型
当插入的值的key在map中不存在,则插入成功,返回插入元素的迭代器和true
当插入的值的key在map中已经存在,则插入失败,返回插入元素的迭代器和false
最后直接返回pair对象即可.
pair<Node*, bool> Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return make_pair(_root, true);
}
// 找到存储位置,把数据插入进去
Node* parent = _root, * cur = _root;
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
{
return make_pair(cur, true);
}
}
cur = new Node(kv);
Node* newnode = cur;
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
// 控制平衡
// 1、更新平衡因子
// 2、如果出现不平衡,则需要旋转
//while (parent)
while (cur != _root)
{
if (parent->_left == cur)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
// parent所在的子树高度变了,会影响parent->parent
// 继续往上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//parent所在子树已经不平衡,需要旋转处理一下
if (parent->_bf == -2)
{
if (cur->_bf == -1)
{
// 右单旋
RotateR(parent);
}
else // cur->_bf == 1
{
RotateLR(parent);
}
}
else // parent->_bf == 2
{
if (cur->_bf == 1)
{
// 左单旋
RotateL(parent);
}
else // cur->_bf == -1
{
RotateRL(parent);
}
}
break;
}
else
{
// 插入节点之前,树已经不平衡了,需要检查
assert(false);
}
}
return make_pair(newnode, true);
}
这里博主没有实现迭代器,等到用红黑树封装set和map的时候在实现迭代器。
室友跟我说AVL树的删除简单了解就可以了。
AVL树的性能:
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即(log2N) 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
AVL树的完整代码:
https://gitee.com/ge-xiaoyu/testc-plus/commit/a7b3378b92ab983968a42119c91a480faa10f109
🍁🍁博主水平有限,如有错误,直接留下评论即可!
🍁🍁感觉还是把握住了!
🍁🍁欢迎一键三连!一起把握住AVL树! 🍁🍁