目录
🌈前言
本篇文章进行数据结构中AVL树的学习!!!
🌷1、AVLTree的概念
AVL树的由来:
- 前面讲了二叉搜索树的不足之处,它虽然可以缩短搜索效率,但是最坏情况会退化成单支树。所以就有了二叉平衡搜索树(AVLTree),它解决了这个弊端,让树保持平衡
历史:
- 俄罗斯的数学家 G.M.Adelson-Velskii 和 E.M.Landis 在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称“平衡因子”)的绝对值不超过2/-2(可以为0/1/-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)
🌹1.2、AVLTree节点的定义
template <typename K, typename V>
struct AVLTNode
{
AVLTNode(const pair<K, V>& _kv)
: kv(_kv)
, left(nullptr)
, right(nullptr)
, parent(nullptr)
, _bf(0)
{}
pair<K, V> kv; // 存储节点值 -- first and second
AVLTNode<K, V>* left;
AVLTNode<K, V>* right;
AVLTNode<K, V>* parent;
int _bf; // 平衡因子,控制树的平衡
};
注意:
- 这里的AVLTree节点存储的值是"键值对"
- AVLTree是通过三叉链来控制的,二叉链也可以,就是麻烦了一些
- _bf是每个节点的平衡因子,用来控制树的平衡
🌿1.2、AVLTree的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
- 按照二叉搜索树的方式插入新节点(升序:值比根小插入左边,反之右边)
- 调整节点的平衡因子
- 按搜索树规则插入,插入后链接双亲节点(parent)
bool insert(const pair<K, V>& kv)
{
if (root == nullptr) {
root = new Node(kv);
root->_bf = 0;
return true;
}
Node* cur = root;
Node* parent = nullptr;
// 按二叉搜索树的特点进行插入
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 false;
}
}
Node* newNode = new Node(kv);
cur = newNode; // 更新cur,待链接父节点
if (parent->kv.first > kv.first) {
parent->left = newNode;
}
else {
parent->right = newNode;
}
cur->parent = parent;
}
注意:按二叉搜索树规则插入,AVLTree为三叉链,链接新节点需要更新双亲节点
- 插入后调整平衡因子,平衡因子可以是0/1/-1,如果为2/-2需要旋转调整
while (parent) // 一直向上回溯调整到祖先结束
{
// 如果插入节点在左边,则双亲节点平衡因子减1,右边加1
if (cur == parent->left) {
--parent->_bf;
}
else {
++parent->_bf;
}
// 如果双亲节点平衡因子为0,则不需要调整 -- 1 or -1 > 0 -- 插入节点填补了矮的节点(插入左边,且右边有节点)
if (parent->_bf == 0) {
break;
}
// 如果父节点平衡因子为1/-1,则继续调整 -- 0 > 1 or -1插入节点导致双亲节点变更或变矮(插入左边,且右边无节点)
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = cur->parent;
parent = parent->parent;
}
// 后面讲旋转会处理
else if (parent->_bf == 2 || parent->_bf == -2)
{}
}
注意:
- 如果cur插入到parent的左侧,只需给parent的平衡因子-1即可
- 如果cur插入到parent的右侧,只需给parent的平衡因子+1即可
- 平衡因子为1/-1时,需要更新cur和parent继续向上调整平衡因子
- 平衡因子为0时,说明这棵树已平衡,跳出循环
🌺1.3、AVLTree的旋转
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
- 新节点插入较高右子树的右侧 — 右右:左单旋(根平衡因子为2,右子树为1)
重点:展开抽象图的几种情况
// 左单旋 -- 新节点插入较高右子树的右侧 -- 右右
void RotateL(Node* parent)
{
Node* subR = parent->right;
Node* subRL = subR->left;
parent->right = subRL;
if (subRL != nullptr) {
subRL->parent = parent; // 如果左节点为空,则会造成空指针访问
}
Node* PpNode = parent->parent; // 后面parent不为根节点时,需要保存parent01的双亲节点进行链接
subR->left = parent;
parent->parent = subR;
// 一开始根节点可能是parent,旋转后要进行处理 -- 根应改为subR,根的双亲节点为nullptr
if (parent == root) {
root = subR;
root->parent = nullptr;
}
else {
// parent可能为整颗树的局部子树 -- 旋转后树的结构就乱了,需要将parent的双亲节点指向正确的位置
if (parent == PpNode->left) {
PpNode->left = subR;
}
else {
PpNode->right = subR;
}
subR->parent = PpNode;
}
// 更新平衡因子
parent->_bf = 0;
subR->_bf = 0;
}
注意:
- 旋转时,需要注意双亲节点要重新调整,很多人会忘记
- 旋转后,subRL的双亲节点需要链接parent,subRL可能为空,需要判空
- 如果根为旋转节点,旋转后subR为根,需要更新root和它的双亲
- 如果为局部子树,需要保存parent的父节点来链接subR,因为旋转后无法找到它
- 新节点插入较高左子树的左侧—左左:右单旋(根节点平衡因子为-2,左子树为-1)
重点:展开抽象图的几种情况
// 右单旋 -- 新节点插入较高左子树的左侧 -- 左左
void RotateR(Node* parent)
{
// 旋转方法与左单旋一致
Node* subL = parent->left;
Node* subLR = subL->right;
parent->left = subLR;
if(subLR != nullptr)
subLR->parent = parent;
Node* PpNode = parent->parent;
subL->right = parent;
parent->parent = subL;
if (parent == root)
{
root = subL;
root->parent = nullptr;
}
else
{
if (PpNode->left == parent) {
PpNode->left = subL;
}
else {
PpNode->right = subL;
}
subL->parent = PpNode;
}
parent->_bf = 0;
subL->_bf = 0;
}
注意:右旋与左旋差不多这里就不画图了,参考左旋
- parent的左节点链接subLR
- subL的右子树链接parent,其余细节问题根左旋一样
- 新节点插入较高左子树的右侧 — 左右:先左单旋再右单旋
重点:展开抽象图的几种情况
// 左右双旋 -- 左子树高,左子树的右子树矮 subL: 1 subLR: -1
void RotateLR(Node* parent)
{
// 记录平衡因子和节点,待更新
Node* subL = parent->left;
Node* subLR = subL->right;
int bf = subLR->_bf;
// 先subL为根进行旋转,然后通过parent为根进行右旋
RotateL(parent->left);
RotateR(parent);
// 分三种情况,画图分析
if (bf == 0) {
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else 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 {
assert(false);
}
}
注意:
- 将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新
- 双旋的难点不是旋转,而是更新平衡因子,不画图容易出错
- 新节点插入较高右子树的左侧 — 右左:先右单旋再左单旋
- 下面直接列举旋转后更新平衡因子的三种情况:(h == 0)的就不画了
// 右左双旋 -- subR: 1 subRL: -1
void RotateRL(Node* parent)
{
// 记录平衡因子和节点,待更新
Node * subR = parent->right;
Node* subRL = subR->left;
int bf = subRL->_bf;
RotateR(parent->right);
RotateL(parent);
// 画图分析 -- 三种情况
if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else {
assert(false);
}
}
- 总结:
假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑:
- parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR
- 当subR的平衡因子为1时,执行左单旋
- 当subR的平衡因子为-1时,执行右左双旋
- parent的平衡因子为-2,说明pParent的左子树高,设parent的左子树的根为subL
- 当subL的平衡因子为-1是,执行右单旋
- 当subL的平衡因子为1时,执行左右双旋
旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新
🌻1.4、AVLTree的删除(了解)
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不
错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。具体实现可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版
🌻1.5、AVLTree的性能
- AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这
样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N) - 但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
- 因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合