一、AVLTree的概念
1.AVLTree的定义
AVLTree:高度平衡二叉搜索树
①首先要满足是一棵搜索二叉树 ②每个节点左右子树的高度差的绝对值不超过1
2.平衡因子
为了方便AVLTree的实现,这里引入节点平衡因子的概念(这只是其中一种实现方式)
节点平衡因子计算公式:平衡因子 = 右子树高度 - 左子树高度
3.AVLTree节点的定义
template<class K, class V>
struct AVLTreeNode
{
public:
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
,_kv(kv)
{}
public:
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf;//balance factor 平衡因子
std::pair<K, V> _kv;//键值对
};
二、AVLTree的操作
(一)AVLTree的插入
AVLTree的插入比二叉搜索树的插入要复杂,AVLTree的插入大体上分成:插入和旋转。
下面对每一步进行详细的介绍,在最后会有插入函数整体的代码
1.先按搜索树的规则进行插入(和二叉搜索树相比就是多一步连接父亲节点)
//1.先按搜索树的规则进行插入
//空树插入
if(_root == nullptr)
{
_root = new Node(kv);
return true;
}
//非空树插入
Node* cur = _root;
Node* parent = nullptr;
//找位置
while (cur)
{
//要插入的小,走左边
if(kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
//要插入的大,走右边
else if(kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
//不可以插入一样的
else
return false;
}
//找到位置了,插入
cur = new Node(kv);
//连接
if(kv.first < parent->_kv.first)
{
parent->_left = cur;//连孩子
cur->_parent = parent;//连父亲
}
else
{
parent->_right = cur;//连孩子
cur->_parent = parent;//连父亲
}
2.更新平衡因子(只会影响插入节点的祖先)
如果更新完平衡因子,如果平衡,则插入结束;parent指向的节点出现不平衡(平衡因子是-1、
0、1就平衡,否则不平衡),需要旋转处理(在第三点介绍)
现在有一个问题:插入后哪些节点会变得不平衡?答:只会影响插入节点的祖先。
再进一步分析:也不是所有的祖先都会被影响,下面四点就是更新平衡因子的过程
(1)首先:若cur是parent的左孩子,parent->bf--;若cur是parent的右孩子,parent->bf++
(2)更新完parent的bf(即完成第一步),如果parent->bf == 0,更新结束,插入完成
parent->bf == 0,说明parent高度不变。解释:说明更新前,parent的bf是1或-1,现在变成0,
说明把矮的那边填上了。说明以parent为根的树的高度不变,对上层没有影响。
(3)更新完parent的bf(即完成第一步),如果parent->bf==1/-1,继续往上更新
parent->bf==1/-1,说明parent的高度变了。解释:说明更新前,parent的bf是0,现在变成1
或-1,说明以parent为根的树的高度变高了,对上层有影响。
(4)更新完parent的bf,如果parent->bf == 2/-2,就需要旋转处理了
parent->bf == 2/-2,说明以parent为根的树出现了不平衡,需要调整成平衡的。
下面展示更新平衡因子的代码
//2.更新平衡因子
while(parent)
{
if(cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
if(parent->_bf == 0)//说明parent所在的子树的高度不变,更新结束
break;
else if(parent->_bf == 1 || parent->_bf == -1)
{
//说明parent所在的子树的高度变了,继续往上更新
cur = parent;
parent = parent->_parent;
}
else if(parent->_bf == 2 || parent->_bf == -2)
{
//parent所在的子树出现不平衡了,需要旋转处理。
}
}
3.旋转处理
如果出现了不平衡,那么就需要旋转处理。旋转完成之后,得到的新树要满足
①新树还是搜索树 ②新树从不平衡变成平衡的
旋转处理一共分成四种情况:两种单旋、两种双旋。单旋的旋转路径是直线,单旋代码更关注旋
转过程。双旋的旋转路径是折线,可以通过两个单旋来完成一个双旋,但是由于插入的位置不
同,最终平衡因子也会随着插入位置的改变而改变,所以双旋代码的重点是调整平衡因子。
下面用图来解释什么是:旋转路径是直线、旋转路径是折线
(1)单旋:旋转路径是直线
①左单旋
什么情况下要进行左单旋?parent->_bf == 2 并且 cur->_bf == 1 的情况下
可以这样理解:新插入的节点在右边,导致右边多出来了,所以需要往左边旋转。
如何左单旋?(a)subR的左边放在parent的右边 (b)parent变成subR的左边
左旋函数代码:因为AVLTree是用三叉链来表示的,所以不仅需要连接孩子,还需要连接父亲。
连接父亲注意三点:
(a)要更新subRL的父亲,特别注意当subRL为空时,就不需要连接父亲了。
(b)要更新parent的父亲,将parent的父亲更新为subR。
(c)旋转后,新树的树根变成了subR,所以需要更新subR的父亲。
第一种情况:原来parent是这棵树的根,现在subR成了这棵树的根。
第二种情况:parent为根的树只是整棵树的子树。
原来parent为根的树是左子树。
原来parent为根的树是右子树。
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* ppNode = parent->_parent;//指向parent节点的父亲
//1.subR的左边放在parent的右边
//!!因为是用三叉链表示的树,所以不仅需要连接儿子,还需要连接父亲!!
parent->_right = subRL;//连儿子
if(subRL)//连父亲
subRL->_parent = parent;
//2.parent变成subR的左边
//同样 !!因为是用三叉链表示的树,所以不仅需要连接儿子,还需要连接父亲!!
subR->_left = parent;
parent->_parent = subR;
//找subR的父亲
//(1)原来parent是这棵树的根,现在subR成了这棵树的根
if(_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
//(2)parent为根的树只是整棵树的子树
else
{
//连孩子
//parent是一个左子树
if(ppNode->_left == parent)
ppNode->_left = subR;
//parent是一个右子树
else
ppNode->_right = subR;
//连父亲
subR->_parent = ppNode;
}
//更新平衡因子
parent->_bf = subR->_bf = 0;
}
②右单旋
什么情况下要进行右单旋?parent->_bf == -2 并且 cur->_bf == -1 的情况下
可以这样理解:新插入的节点在左边,导致左边多出来了,所以需要往右边旋转。
如何右单旋?(a)subL的右边放在parent的左边 (b)parent变成subL的右边
右旋函数代码:因为AVLTree是用三叉链来表示的,所以不仅需要连接孩子,还需要连接父亲。
连接父亲注意的三点和左旋函数的一样,在这里就不再提了,直接看代码。
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* ppNode = parent->_parent;//指向parent节点的父亲
//1.subL的右边放到parent的左边
parent->_left = subLR;
if(subLR)
subLR->_parent = parent;
//2.parent变成subL的右边
subL->_right = parent;
parent->_parent = subL;
//找subL的父亲
//(1)原来parent是这棵树的根,现在subL成了这棵树的根
if(_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
//(2)parent为根的树只是整棵树的子树
else
{
//连孩子
//parent是一个左子树
if(ppNode->_left == parent)
ppNode->_left = subL;
//parent是一个右子树
else
ppNode->_right = subL;
//连父亲
subL->_parent = ppNode;
}
//更新平衡因子
parent->_bf = subL->_bf = 0;
}
(2)双旋:旋转路径是折线
①左右双旋
什么情况下要进行左右双旋?parent->_bf == -2 并且 cur->_bf == 1 的情况下。
在b和c上插入都是左右双旋,但是这会导致两种插入的subLR->_bf不一样(后面重点说明)
如何左右双旋?先对30的位置进行左单旋,在对90的位置进行右单旋。
左右双旋函数代码:⚠️因为插入的位置不一样导致 更新平衡因子 会有分类讨论。
//左右双旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
if(bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if(bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if(bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
}
②右左双旋
什么情况下要进行右左双旋?parent->_bf == 2 并且 cur->_bf == -1 的情况下。
在b和c上插入都是右左双旋,但是这会导致两种插入的subRL->_bf不一样(后面重点说明)
如何右左双旋?先对90的位置进行右单旋,在对30的位置进行左单旋。
右左双旋函数代码:⚠️因为插入的位置不一样导致 更新平衡因子 会有分类讨论。
//右左双旋
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(subR);
RotateL(parent);
if(bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if(bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if(bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
}
下面展示旋转处理的代码
注意⚠️:进行了旋转处理以后,整棵树就达到了平衡状态,所以直接跳出循环即可。
else if(parent->_bf == 2 || parent->_bf == -2)
{
//parent所在的子树出现不平衡了,需要旋转处理。
if(parent->_bf == 2)
{
if(cur->_bf == 1)
RotateL(parent);
else if (cur->_bf == -1)
RotateRL(parent);
}
else if(parent->_bf == -2)
{
if(cur->_bf == -1)
RotateR(parent);
else if(cur->_bf == 1)
RotateLR(parent);
}
//完成旋转之后,整棵树就达到了平衡状态,所以直接跳出循环即可
break;
}
下面展示插入的全部代码
bool Insert(const pair<K, V>& kv)
{
//1.先按搜索树的规则进行插入
//空树插入
if(_root == nullptr)
{
_root = new Node(kv);
return true;
}
//非空树插入
Node* cur = _root;
Node* parent = nullptr;
//找位置
while (cur)
{
//要插入的小,走左边
if(kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
//要插入的大,走右边
else if(kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
//不可以插入一样的
else
return false;
}
//找到位置了,插入
cur = new Node(kv);
//连接
if(kv.first < parent->_kv.first)
{
parent->_left = cur;//连孩子
cur->_parent = parent;//连父亲
}
else
{
parent->_right = cur;//连孩子
cur->_parent = parent;//连父亲
}
//2.更新平衡因子
while (parent) {
if(cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
if(parent->_bf == 0)//说明parent所在的子树的高度不变,更新结束
break;
else if(parent->_bf == 1 || parent->_bf == -1)
{
//说明parent所在的子树的高度变了,继续往上更新
cur = parent;
parent = parent->_parent;
}
else if(parent->_bf == 2 || parent->_bf == -2)
{
//parent所在的子树出现不平衡了,需要旋转处理。
if(parent->_bf == 2)
{
if(cur->_bf == 1)
RotateL(parent);
else if (cur->_bf == -1)
RotateRL(parent);
}
else if(parent->_bf == -2)
{
if(cur->_bf == -1)
RotateR(parent);
else if(cur->_bf == 1)
RotateLR(parent);
}
//完成旋转之后,整棵树就达到了平衡状态,所以直接跳出循环即可
break;
}
}
return true;
}
(二)AVLTree的删除
AVLTree的删除和插入的过程相似,所以我们对比插入来学习删除。
增【插入】:a、按搜索树的规则插入 b、更新平衡因子(下面对比两者更新平衡因子的不同)
c、更新过程中出现平衡因子为2/-2,则根据情况判断是那种旋转,进行旋转处理。
删【删除】:a、按搜索树的规则删除 b、更新平衡因子(下面对比两者更新平衡因子的不同)
c、更新过程中出现平衡因子为2/-2,则根据情況判断是哪种旋转,进行旋转处理。
介绍删除中平衡因子的更新:首先要明确删除的更新和插入的更新过程基本相反,依然对比介绍
1.右边插入,父亲平衡因子++;左边插入,父亲平衡因子--。
右边删除,父亲平衡因子--;左边删除,父亲平衡因子++。
2.插入后,父亲的平衡因子变成0,说明父亲所在的树高度不变,更新结束。
删除后,父亲的平衡因子变成0,说明父亲所在的树高度变了,继续往上更新。
3.插入后,父亲的平衡因子变成1/-1,说明父亲所在的树高度变了,继续往上更新。
删除后,父亲的平衡因子变成1/-1,说明父亲所在的树高度不变,更新结束。
4.插入/删除后,父亲的平衡因子变成2/-2,说明不平衡,旋转处理。
(三)AVLTree的查改
1.搜索树中key是不允许修改的,因为如果修改了整棵树可能就破坏了。
2.key/value的场景下可以修改value,但是不能修改key。
查和改和二叉树搜索树是一样的,所以这里不在展示代码,在二叉树搜索树的博客中有展示。
(四)AVLTree的验证
验证AVLTree可以分成两步:
1.验证其为二叉搜索树 <==> 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树。
2.验证其为平衡树 <==> 每个节点子树高度差的绝对值不超过1。
三、AVLTree的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这
样可以保证查询时高效的时间复杂度o(logN)。但是如果要对AVL树做一些结构修改的操作,性能
非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一
直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数
为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合用AVL树。