自学内容网 自学内容网

【数据结构】AVL树(C++实现)




前言


二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson - Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/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)。


AVL树节点的定义


template<class T>
struct TreeNode
{
public:
TreeNode(T data)
:_data(data), _bf(0)
, _parent(nullptr), _left(nullptr), _right(nullptr)
{}
public:
T _data;
// Defining balance factor
int _bf;  // 该节点的平衡因子
// 定义成一个三叉链结构,方便后续操作
TreeNode* _parent;  // 该节点的双亲
TreeNode* _left;  // 该节点的左孩子
TreeNode* _right;  // 该节点的右孩子
};


AVL树的插入


因为插入逻辑比较复杂,我在这里就写一个大致思路,具体代码我上传到了git上。

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

此处我写一个大致的代码逻辑,并结合图片给大家看一下:

bool Insert(const T& data)
{
// 1. 先按照二叉搜索树的规则将节点插入到AVL树中
// ...

// 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,
// 并检测是否破坏了AVL树的平衡性

 /*
 cur插入后,parent的平衡因子一定需要调整,
 在插入之前,parent的平衡因子分为三种情况:-1,0, 1, 
 插入时出现以下两种情况:
  1. 如果cur插入到parent的左侧,只需给parent的平衡因子-1即可
  2. 如果cur插入到pParent的右侧,只需给parent的平衡因子+1即可
  
 此时:parent的平衡因子可能有三种情况:0,正负1, 正负2
  1. 如果parent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,
  插入后被调整成0,此时满足AVL树的性质,插入成功
  2. 如果parent的平衡因子为正负1,说明插入前parent的平衡因子一定为0,
  插入后被更新成正负1,此时以pParent为根的树的高度增加,需要继续向上更新
  3. 如果parent的平衡因子为正负2,则parent的平衡因子违反平衡树的性质,
  需要对其进行旋转处理
 */
while (parent)
{
// 更新双亲的平衡因子
if (cur == parent->_pLeft)
parent->_bf--;
else
parent->_bf++;
// 更新后检测双亲的平衡因子
if (0 == parent->_bf)
{
break;
}
else if (1 == pParent->_bf || -1 == pParent->_bf)
{
// 插入前双亲的平衡因子是0,插入后双亲的平衡因为为1 或者 -1 ,说明以双亲为根的二叉树
// 的高度增加了一层,因此需要继续向上调整
cur = pParent;
pParent = pCur->_pParent;
}
else
{
// 双亲的平衡因子为正负2,违反了AVL树的平衡性,需要对以parent
// 为根的树进行旋转处理
if (2 == pParent->_bf)
{
// ...
}
else
{
// ...
}
}
}
return true;
}

在这里插入图片描述

插入节点会影响新增节点的部分祖先,
更新规则:

  • c是p的左边,p->_bf--
  • c是p的右边,p->_bf++

是否要继续更新取决于p的高度是否变化,是否会影响爷爷节点。

在上面这张图的基础上,插入11:
请添加图片描述
我们可以看到:

  • 更新后,p->_bf == 0,p所在的高度不会改变,不会影响爷爷节点。说明更新前,p->_bf是1或者-1,p的矮的那一边插入了新节点,左右高度均衡了,p的高度不变,不会影响爷爷,更新结束。
  • 更新后,p->_bf == 1 / -1,p所在的子树高度变了,会影响爷爷。说明更新前,p->_bf是0,p的有一边插入,p变得不均衡,但不违反规则,p的高度变了,会影响爷爷,继续往上更新

在上面这张图的基础上,再插入1:
在这里插入图片描述

  • 更新后,p->_bf == 2 / -2,说明p所在的子树违反了平衡规则,需要进行旋转处理。


AVL树的旋转


如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:

1. 新节点插入较高左子树的左侧—左左:右单旋
在这里插入图片描述

/*
  上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,
  30左子树增加了一层,导致以60为根的二叉树不平衡,
  要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,
  即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,
  而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,
  旋转完成后,更新节点的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:
  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树
     如果是根节点,旋转完成后,要更新根节点
     如果是子树,可能是某个节点的左子树,也可能是右子树
    
  大家在此处可举一些详细的例子进行画图,考虑各种情况,加深旋转的理解
*/
void RotateR(Node* parents)
{
// SubL: parents的左孩子
// SubLR: parents左孩子的右孩子,注意:该节点可能为空
Node* SubL = parents->_left;
Node* SubLR = SubL->_right;

// 旋转完成之后,30的右孩子作为双亲的左孩子
parents->_left = SubLR;
// 如果30的左孩子的右孩子存在,更新该节点的双亲
if (SubLR != nullptr)
SubLR->_parent = parents;

// 60 作为 30的右孩子
SubL->_right = parents;
// 因为60可能是棵子树,因此在更新其双亲前必须先保存60的双亲
Node* ppnode = parents->_parent;

    // 更新60的双亲
parents->_parent = SubL;

// 更新30的双亲
SubL->_parent = ppnode;

// 如果60是根节点,更新指向根节点的指针
if (_root == parents)
{
_root = SubL;
SubL->_parent = nullptr;
}
else
{ 
// 如果60是子树,可能是其双亲的左子树,也可能是右子树
if (parents == ppnode->_left)
ppnode->_left = SubL;
else
ppnode->_right = SubL;
}

// Renewal balance factor
parents->_bf = 0;
SubL->_bf = 0;
}

2. 新节点插入较高右子树的右侧—右右:左单旋

在这里插入图片描述
实现及情况考虑可参考右单旋:

void RotateL(Node* parents)
{
Node* SubR = parents->_right;
Node* SubRL = SubR->_left;

parents->_right = SubRL;
if (SubRL != nullptr)
SubRL->_parent = parents;

SubR->_left = parents;

Node* ppnode = parents->_parent;
parents->_parent = SubR;

SubR->_parent = ppnode;
if (_root == parents)
{
_root = SubR;
SubR->_parent = nullptr;
}
else
{
if (parents == ppnode->_left)
ppnode->_left = SubR;
else
ppnode->_right = SubR;
}

// Renewal balance factor
parents->_bf = 0;
SubR->_bf = 0;
}

3. 新节点插入较高左子树的右侧—左右:先左单选再右单旋
在这里插入图片描述
将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新。
旋转之前先看60的平衡因子是多少,旋转结束以后,根据60的平衡因子更新结果。
这里平衡因子的更新有三种情况:

  1. 旋转之前,60的平衡因子为-1
    在这里插入图片描述

  2. 旋转之前,60的平衡因子为1
    在这里插入图片描述

  3. 旋转之前,60的平衡因子为0
    在这里插入图片描述

从这三张图中,可以发现,不管旋转之前60的平衡因子是多少,最后都会变成0(在单旋中,已经将它置0),所以我们只用观察30,90 的平衡因子。而不管这棵树怎么变,它必定会遵守搜索二叉树的规则,所以在这里我只是用了一个示例向大家展示这三种情况。不管这棵树的形状是什么样的,只要它满足搜索二叉树的规则,那么左右双旋只会有这几种情况。

// 旋转之前,60的平衡因子可能是-1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进行调整
void RotateLR(Node* parents)
{
Node* SubL = parents->_left;
Node* SubLR = SubL->_right;

// 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节点的平衡因子
int bf = SubLR->_bf;

 // 先对30进行左单旋
RotateL(SubL);

 // 再对90进行右单旋
RotateR(parents);

// Renewal balance factor
if (bf == 0)
{
parents->_bf = 0;
SubL->_bf = 0;
}
else if (bf == 1)
{
parents->_bf = 0;
SubL->_bf = -1;
}
else if (bf == -1)
{
parents->_bf = 1;
SubL->_bf = 0;
}
}

4. 新节点插入较高右子树的左侧—右左:先右单旋再左单旋

在这里插入图片描述

这里与左右双旋相似,将双旋变成单旋后再旋转,即:先对90进行右单旋,然后再对30进行左单旋,旋转完成后再考虑平衡因子的更新。
旋转之前先看60的平衡因子是多少,旋转结束以后,根据60的平衡因子更新结果。
这里平衡因子的更新有三种情况:

  1. 旋转之前,60的平衡因子为1
    在这里插入图片描述

  2. 旋转之前,60的平衡因子为-1
    在这里插入图片描述

  3. 旋转之前,60的平衡因子为0
    在这里插入图片描述

void RotateRL(Node* parents)
{
Node* SubR = parents->_right;
Node* SubRL = SubR->_left;
int bf = SubRL->_bf;
RotateR(SubR);
RotateL(parents);
// Renewal balance factor
if (bf == 0)
{
parents->_bf = 0;
SubR->_bf = 0;
}
else if (bf == 1)
{
parents->_bf = -1;
SubR->_bf = 0;
}
else if (bf == -1)
{
parents->_bf = 0;
SubR->_bf = 1;
}
}

总结
假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑

  1. parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为SubR
    当SubR的平衡因子为1时,执行左单旋
    当SubR的平衡因子为-1时,执行右左双旋
  2. parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为SubL
    当SubL的平衡因子为-1时,执行右单旋
    当SubL的平衡因子为1时,执行左右双旋
    旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新。

AVL树的旋转只有以上情况,不可能会出现其他情况了。


AVL树的验证


AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    • 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
    • 节点的平衡因子是否计算正确
bool Isbalance()
{
int Hight = 0;
return _Isbalance(_root, Hight);
}
// 为了提高效率,可以走后序来计算每个节点的左子树和右子树的高度差为多少
// 通过Hight来保存每个节点的最高的子树的高度为多少
bool _Isbalance(Node* root, int& Hight)
{
if (root == nullptr)
return true;
int leftHight = 0;
int rightHight = 0;
if (!_Isbalance(root->_left, leftHight) || !_Isbalance(root->_right, rightHight))
{
// 如果root的左子树或者右子树不平衡,就直接返回false
return false;
}
// 判断每个节点的左右子树高度差的绝对值不超过1
if (abs(rightHight - leftHight) >= 2)
{
std::cout << "balance factor error, the tree is unbalanced\n";
return false;
}
// 判断每个节点的右子树的最大高度与左子树的最大高度的差值是否等于该节点的平衡因子
else if (rightHight - leftHight != root->_bf)
{
std::cout << root->_data << " the balance factor is wrong\n";
return false;
}
// 保存左子树和右子树较高的高度
Hight = leftHight > rightHight ? leftHight + 1 : rightHight + 1;
return true;
}

在这里插入图片描述
这段代码是随机生成10000个数,插入到我自己写的一棵AVL树中,插完这10000个树以后,我对这棵树进行了一下判断是否平衡,结果是平衡的,证明我的插入逻辑是没有问题的(我测试了不止这一组数据)。


AVL树的删除


因为插入删除比较复杂,我在这里就写一个大致思路,具体代码我上传到了git上。

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不
过与插入不同的是,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。

这里更新平衡因子的逻辑与插入更新平衡因子的逻辑恰恰是相反的。

在这里插入图片描述

删除节点会影响删除节点的部分祖先(如果是删除根节点,也会找一个可以替代的节点交换值再删除,具体可以参考搜索二叉树的删除部分)。
更新原则:

  • c是p的左边,p->_bf++
  • c是p的右边,p->_bf--

是否要继续更新取决于p的高度是否变化,是否会影响爷爷节点。

在上面这张图的基础上,删除7:
在这里插入图片描述
在上面这张图的基础上,再删除3:
在这里插入图片描述
在上面这张图的基础上,再删除14:
在这里插入图片描述

从上面三个动图可以分析出:

  • 更新后p->_bf == 1 / -1,p所在的子树高度不变,不会影响爷爷。说明更新前,p->_bf是0,p的两边是均衡的,p的有一边删除了,p变得不均衡,但不违反规则,p的高度不变,不会影响爷爷,更新结束。
  • 更新后,p->_bf == 0,p所在的子树的高度变了,会影响爷爷。说明更新前,p->_bf == 1 / -1,p的有一边删除了,p的左右变得均衡,p的高度变了,会影响爷爷,继续往上更新。
  • 更新后,p->_bf == 2 / -2,说明p所在的子树违反了平衡规则,需要进行旋转处理。


AVL树的性能与源码


AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2(N) log2(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

源码地址:https://gitee.com/ASL498/data-structure/tree/master/AVLTree


原文地址:https://blog.csdn.net/weixin_73914025/article/details/139559694

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!