【C++升华篇】学习C++就看这篇--->AVLtree深度剖析&模拟建立

个人主页:HABuo
个人专栏:《C++系列》《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》
⛰️ 如果再也不能见到你,祝你早安,午安,晚安
目录
前言:
前面我们在二叉搜索树部分提到如果插入数据本来就是有序的,那么这棵树的查找效率将会退化成O(N),为了解决这个问题,引入了AVLtree,因此在实际应用中二叉搜索树的使用不多。这篇博客我们就来揭开AVLtree(高度平衡二叉搜索树)的神秘面纱。
如果听了上面的话你不知道在讲什么
请一定先阅读这篇文章:
【C++升华篇】学习C++就看这篇--->二叉搜索树深度剖析
本篇重点:
本篇文章着重讲解AVL树的概念以及定义,并且在模拟实现AVL树前,将AVL树的插入的情况做系统分析,插入情况有可能在面试当中被问的,但是被手撕的概率不太高,因为这玩意又臭又硬,但是思想我们还是要学会并且记住它,这是我们能力体现的最好方式,最后模拟实现AVL树的插入操作,进一步理解它!
一、AVL树的概念以及特性
什么是AVL树?
AVL树是一种自平衡的二叉搜索树,由两位苏联数学家Adelson-Velsky和Landis在1962年发明。它的核心特点是:任何节点的左右子树高度差不超过1。
为什么需要AVL树?
①普通二叉搜索树在极端情况下会退化成链表,时间复杂度从O(log n)变为O(n)
② AVL树通过自动平衡保持树的高度最小,确保各种操作的时间复杂度为O(log n
核心概念:平衡因子
平衡因子 = 右子树高度 - 左子树高度(或者左−右也可以)
AVL树要求每个节点的平衡因子只能是 -1、0 或 1。
①平衡因子为 0:表示左右子树高度相等。
②平衡因子为 1:表示右子树比左子树高1
③平衡因子为 -1:表示右子树比左子树低1
二、AVL的模拟实现
1.AVL树的基本结构
AVL树的底层结构在代码实现中使用节点结构体来表示。节点结构体 保存每个节点的数据、指向左右子节点、父节点的指针、以及平衡因子;树类管理插入、删除等操作。
template
struct AVLtreeNode {
AVLtreeNode* __left;//采用三叉链的结构容易实现
AVLtreeNode* __right;
AVLtreeNode* __parent;
int _bf;//平衡因子
pair _kv;//所要存储的值
AVLtreeNode(const pair& kv)//写自定义的构造函数防止深拷贝等不必要的问题
:__left(nullptr)
,__right(nullptr)
,__parent(nullptr)
,_kv(kv)
,_bf(0)
{}
};
2.AVL树的插入操作
AVL树的插入同样要遵守二叉搜索树的插入规则,即都是在叶子节点进行插入,第一个插入的元素就是根节点,后面插入的元素根据性质判断插入位置。比如在下面这颗树中插入10和36:


这一步在二叉搜索树的部分已经详细的进行了讲解,我们在此唯一需要更新的地方就是平衡因子部分
平衡因子更新

可以观察到当我们插入一个36时,它的父亲的平衡因子就变为了1,进一步的观察到41节点的平衡因子变成了-1,更进一步的发现根节点的平衡因子变成了2,发现什么规律了吗?这些节点均是36的祖先,由此可以得到,当插入节点,需要更新平衡因子的部分是且只能是它的祖先节点。
相信你会有个疑惑,难道每个插入节点,我们需不需要更新平衡因子,难道都要看到根节点?很好,你抓住了问题的核心。接下来更新平衡因子的关键:
①插入一个节点,如果该节点在它父节点的左,那么父节点平衡因子--,如果在右,那么父节点平衡因子++。
②如果更新完父节点的平衡因子之后,为0表示父节点的高度不变,更新结束,插入完成。
③如果为1、-1那么表示父节点的高度变了,继续向上更新。
④如果为2、-2那么表示该节点失衡,需要进行旋转。
解释:
第一点,相信很好理解,因为我们平衡因子的公式就是右节点 - 左节点。第二点,插入一个节点之后该节点的父节点平衡因子为0,说明这个父节点在插入之前平衡因子只能是1/-1,你品你细品,不解释,因此插入节点并没有改变它的高度,更新到此就可以结束。第三点,如果为1/-1,是不是表示之前为0?恭喜你会抢答了,是,不解释。第四点,如果为2/-2,说明之前是1/-1,是,但是已经不重要了,因为2/-2已经超出了我们所允许的范围,因此更新到此为止,我们需要对其进行旋转,使它的平衡因子还原到允许范围之内。(这里有一个很重要的思想,就是为什么到了2/-2这里就更新结束了呢?答案就在于旋转之后子树的高度还原成了插入之前,并不对该子树的父节点的平衡因子造成什么影响)什么是旋转看下文。
到此我们可以对上述的逻辑进行源码实现了,请看下述代码:
bool insert(const pair& kv)//总体思路是 1. 按照二叉搜索树的插入思路,只不过在其中插入平衡因子的概念
{ // 每一步的插入都需要更新平衡银子。2. 如果平衡因子出现了不正常,就需要进行旋转来维持平衡
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->__right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->__left;
}
else
return false;
}
Node* temp = new Node(kv);
cur = temp;
cur->__parent = parent;
if (cur->_kv.first > parent->_kv.first)
parent->__right = cur;
else if (cur->_kv.first > parent->_kv.first)
parent->__left = cur;
//上面和我们二叉搜索树的插入没有任何区别
//下面更新平衡因子
while (parent)
{
if (parent->__right == cur)
parent->_bf++;
else if (parent->__left == cur)
parent->_bf--;
if (parent->_bf == 0)
break;
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->__parent;
}
else if (abs(parent->_bf) == 2)//这里就是平衡因子失衡的状况
{
// 旋转处理
}
else
{
// 插入之前这棵树就有2/-2 bf的节点,这棵树之前就不是AVL树
assert(false);
}
}
return true;
}
旋转原理
先看下面这棵树:


很明显,这是一颗左单树,不符合AVL树的定义,此时我想把它变成一颗AVL树,该怎么做呢。我们把 5 的父节点7 “向下压”,变成 5 的右孩子,这样就成为了一颗AVL树。这个“向下压”的过程 就叫做旋转。由于7 是“向下压”成 5 的右孩子,所以整个过程又叫右单旋。那如果节点5 本身就有右孩子呢,只需要把7 的左孩子指向5 的右孩子就可以了:

既然有右旋,那当然也有左旋。

同理地,把这颗右单树变成AVL树的过程,就称之为左单旋。如果节点5 本身就有左孩子,那么需要把3 的右孩子指向5 的左孩子。
那什么时候用右旋,什么时候用左旋呢?当节点平衡因子为2时,左旋;平衡因子为-2时,右旋。
但是以上两种旋转方法只适用于单子树的情况,即所有子节点都在左侧或者右侧,遇到下面这种情况就不可行了:

此时节点3的平衡因子为2,用左旋,但可以看到,旋转之后仍然不是AVL树,那么该怎么办呢?
既然一次旋转不行,那我们就试试旋转两次嘛,我们先对节点5 进行一次右旋,再对节点3 进行一次左旋,结果终于是一颗AVL树了:

这种先右旋再左旋的操作,我们称之为右左双旋。
下面来看看左右双旋的情况:

综上,我们一共介绍了四种旋转方式,分别是:右单旋,左单旋,右左单旋,左右单旋。接下来我们就对它们逐一的进行代码实现。

void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
Node* parentParent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
subR->_bf = parent->_bf = 0;
}
代码解析:
我们定义一个左旋函数,参数就是平衡因子失衡的那个节点即是需要旋转的那个节点。主要思路:1.需要把该节点(父节点)的父节点指针链到我的右孩子上 2.如果右孩子有左孩子需要把它的左孩子给该节点右孩子指针 3.右孩子的左指针链该节点 4.判断新的父节点它的父指针是否链接完毕。简单的说就是两个注意事项:1.旋转前判断右孩子是否有左树 2.旋转之后节点与节点之间的链接是否完整。

void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
parentParent->_left = subL;
else
parentParent->_right = subL;
subL->_parent = parentParent;
}
parent->_bf = subL->_bf = 0;
}
代码解析:
右旋与左旋完全是同样的道理,只需要反转一下思维即可。
简单的说也是两个注意事项:1.旋转前判断左孩子是否有右树 2.旋转之后节点与节点之间的链接是否完整。

void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
if (bf == 0)
{
subL->_bf = 0;
parent->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1)
{
subL->_bf = -1;
parent->_bf = 0;
subLR->_bf = 0;
}
else if (bf == -1)
{
subL->_bf = 0;
parent->_bf = 1;
subLR->_bf = 0;
}
else
{
assert(false);
}
}

代码解析:
我们定义一个左右双旋函数,参数就是平衡因子失衡的那个节点即是需要左右双旋的那个节点。事实上,我们对左旋和右旋已经实现,直接套用它们的函数即可,关键是逻辑,1.双旋,是所传参数的节点的左孩子的右树开始,即该右树的父节点左旋,该父节点的父节点(即所传参数)进行右旋。2.平衡因子调节,观察上图插入之后60的平衡因子是且只能是1、0、-1,如果60的平衡因子是-1,那么b的高度一定和a是一样的,且一定会链到30的右树上,那么30的平衡因子一定会为0,此时c(h-1)就会跑到90的左树,那么90的平衡因子就一定会变成1,调整后的根节点(60)的平衡因子一定会为0。那么同理,如果60的平衡因子是1,那么b的高度一定和a、d是一样的,且一定会跑到90的左树,那么90的平衡因子就一定会变成0,相应的30的平衡因子一定是-1,调整后的根节点(60)的平衡因子一定会为0。如果60的平衡因子是0,说明插入节点之后60的左右子树高度都为h(相信你会说为什么不都为h-1,如果插入之后是h-1,那么之前就是h-2,我们还是在调整90这个节点吗?),因此调整后30和90的平衡因子为0。这个关键就是平衡因子的最终确立,希望大家对照图形多捋几遍,自然就理解了。

void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
代码解析:
相信在左右双旋的加持下,这个会迎刃而解。两点:1.90右旋,30左旋。2.调整之后平衡因子的确立,还是分为插入节点之后60的平衡因子为0还是1还是-1。
旋转原理学习完了之后,我们就需要将插入中平衡因子失衡部分的逻辑进行补充,看下述代码:
else if (abs(parent->_bf) == 2)//这里就是平衡因子失衡的状况
{
// 旋转处理
if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
else
{
assert(false);
}
break;
}
逻辑参照图很容易得到,我们可以稍加记忆一下:1.父子平衡因子同符号说明插入节点和祖先节点在一点线上,一定是单旋。2.父子节点平衡因子异号说明插入节点和祖先节点不在一条线上,是一条折线,那么是双旋。最后再进行判断是什么方向旋转
3.AVL树的验证
可以在AVLtree类里面插入下面的函数来进行验证
int Height(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
bool IsBalance(Node* root)
{
if (root == nullptr)
return true;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return abs(leftHeight - rightHeight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
bool IsBalance()
{
return IsBalance(_root);
}
三、总结
AVLtree时间复杂度分析
| 操作 | 平均情况 | 最坏情况 |
|---|---|---|
| 搜索 | O(log n) | O(log n) |
| 插入 | O(log n) | O(log n) |
| 删除 | O(log n) | O(log n) |
关键要点总结
①平衡保证:AVL树通过严格的平衡条件确保树的高度始终为O(log n)。
②旋转操作:四种旋转情况处理不同的不平衡场景。
③高度维护:每次插入删除后都需要更新高度并检查平衡。
④性能稳定:所有操作都保证对数时间复杂度。


浙公网安备 33010602011771号