
C++专栏:C++_Yupureki的博客-CSDN博客
目录
[1. AVL树的概念](#1. AVL树的概念)
[2. AVL树的实现](#2. AVL树的实现)
[2.1 AVL树的结构](#2.1 AVL树的结构)
[2.2 AVL树的插入](#2.2 AVL树的插入)
[2.2.1 AVL树插入值的大概过程](#2.2.1 AVL树插入值的大概过程)
[2.2.2 平衡因子更新](#2.2.2 平衡因子更新)
[2.2.3 插入结点及更新平衡因子的代码实现](#2.2.3 插入结点及更新平衡因子的代码实现)
[2.3 旋转](#2.3 旋转)
[2.3.1 旋转的原则](#2.3.1 旋转的原则)
[2.3.2 左单旋](#2.3.2 左单旋)
[2.3.3 右单旋](#2.3.3 右单旋)
[2.3.4 左右双旋](#2.3.4 左右双旋)
[2.3.5 右左双旋](#2.3.5 右左双旋)
[2.4 AVL树的查找](#2.4 AVL树的查找)
[2.5 AVL树平衡检测](#2.5 AVL树平衡检测)
[2.6 AVL树的删除](#2.6 AVL树的删除)
上一篇:从零开始的C++学习生活 11:二叉搜索树全面解析-CSDN博客
前言
在前面,我们学习了二叉搜索树。二叉搜索树(BST)作为一种常见的数据结构,虽然提供了快速的查找、插入和删除操作,但在最坏情况下(如插入有序数据)会退化成链表,导致时间复杂度从理想的O(log N)恶化到O(N)。为了解决这个问题,两位前苏联科学家G. M. Adelson-Velsky和E. M. Landis在1962年的论文《An algorithm for the organization of information》中提出了AVL树------一种自平衡二叉搜索树。
AVL树通过严格控制树的平衡性,确保在任何情况下树的高度都保持在O(log N)级别,从而保证了所有操作的时间复杂度都为O(log N)。我将详细解析AVL树的原理、实现方法以及平衡维护机制。

1. AVL树的概念
AVL树是最先发明的自平衡二叉查找树
其具有以下特性:
它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。AVL树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。
AVL树实现这里我们引入一个平衡因子(balance factor)的概念,每个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度,也就是说任何结点的平衡因子等于0/1/-1

AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。
思考一下为什么AVL树是高度平衡搜索二叉树,要求高度差不超过1,而不是高度差是0呢?因为很多情况无法保证高度差一定为0。比如一棵树是2个结点,4个结点等情况下,高度差最好就是1,无法做到高度差是0。
AVL树整体结点数量和分布和完全二叉树类似,高度可以控制在 logNlogN ,那么增删查改的效率也可以控制在 O(logN)O(logN) ,相比二叉搜索树有了本质的提升。
2. AVL树的实现
2.1 AVL树的结构
一般AVL树我们使用前面将的key/value组合,而在这里我们使用一个叫pair的类封装key和value(pair为库文件中的类)。这是一个专门处理两个值映射关系的类
cpp
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; // balance factor
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
2.2 AVL树的插入
2.2.1 AVL树插入值的大概过程
-
插入一个值按二叉搜索树规则进行插入。
-
新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点->根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了。
-
更新平衡因子过程中没有出现问题,则插入结束。
-
更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。
2.2.2 平衡因子更新
更新原则:
-
平衡因子=右子树高度-左子树高度
-
只有子树高度变化才会影响当前结点平衡因子。
-
插入结点,会增加高度,所以新增结点在parent的右子树,parent的平衡因子++,新增结点在parent的左子树,parent平衡因子--
-
parent所在子树的高度是否变化决定了是否会继续往上更新

我们插入3后,其父节点4,6,3,8都发生了平衡因子变化,而其余的节点不会变化,因为只有平衡因子等于右子树高度-左子树高度,只有子树高度变化才会引起平衡因子的变化
如果平衡因子变为-1或者1,说明是从0变过去的,也就是增加了高度,所以还得对上面进行调整
如果是0,说明是从-1或者1变过去的,就相当于原来一个节点只有一个孩子,现在补了一个,从-1或者1变成了0,那高度变化了吗?没有,所以不用向上调整
当为2或者-2时,树不平衡,需要进行旋转调整
2.2.3 插入结点及更新平衡因子的代码实现
cpp
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
// 更新平衡因子
while (parent)
{
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
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)
{
// 不平衡了,旋转处理
break;
}
else
{
assert(false);
}
}
return true;
}
2.3 旋转
在上面平衡因子的更新中,我们发现有的节点已经不满足平衡因子的绝对值小于2的条件了,变成了2或者-2,因此我们需要对树进行一系列的操作,使得平衡因子满足规则
2.3.1 旋转的原则
-
保持搜索树的规则
-
让旋转的树从不满足变平衡,其次降低旋转树的高度
旋转总共分为四种:左单旋、右单旋、左右双旋、右左双旋。
2.3.2 左单旋
当某个节点的右子树比左子树高2,且右子树的右子树比左子树高时,需要进行左单旋。

平衡因子更新到2,那么一定是从1更新到2,即右子树的高度+1
在这里这种情况左单旋还无法解决,我们把3和1删掉,假设4是新插入的节点
我们拆分这个树单独查看

我们可以把左单旋看作往左边"压",因为3节点翘得太高了,以至于导致平衡因子失衡。所以把3给压下去,换6上来,同时让3指向6的左孩子4。不要忘记让8之前指向的是3,现在要改为6
左单旋代码实现:
cpp
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 (parentParent == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
parent->_bf = subR->_bf = 0;
}
2.3.3 右单旋
右单旋其实和左单旋十分相似,只是旋的方向相反而已

(左图4的平衡因子是0)
我们这次把6压下去,换4上来,然后6指向4的右孩子7,8指向4
右单旋代码实现:
cpp
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 (parentParent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
parent->_bf = subL->_bf = 0;
}
2.3.4 左右双旋
我们先看下面两个树

看起来区别就是4的左右孩子不一样
然后6肯定是失衡了的,我们都进行右旋看看


我们会发现上面的情形进行右单旋后,4上去后平衡因子却成了2
因为右旋后6要指向5,6本来就下去了,6下面再接个5,不就造成了平衡因子再次失衡吗
所以发生这种情况的条件是,4的右子树高度高于左子树,即4的平衡因子是1
因为当平衡因子是1的时候,就会发生错误,所以我们只需要改变4的平衡因子即可,就是对4进行左单旋

让4和5是往左下斜,而不是往右下斜,这样再次进行右旋即可
cpp
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 0)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else if(bf == 1)
{
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
2.3.5 右左双旋
当某个节点的右子树比左子树高2,且右子树的左子树比右子树高时,需要进行右左双旋:先对右子树进行右旋,再对当前节点进行左旋。
与左右双旋的案例十分相似,我们不再做过多的讨论
cpp
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);
}
}
2.4 AVL树的查找
AVL树的查找与普通二叉搜索树相同,但由于AVL树是平衡的,查找效率始终为O(log N)。
cpp
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first <= key)
{
cur = cur->_right;
}
else
{
cur = cur->_left;
}
}
return nullptr;
}
2.5 AVL树平衡检测
为了验证实现的AVL树是否正确,我们可以通过检查左右子树高度差的程序进行反向验证,同时检查结点的平衡因子更新是否出现了问题。
cpp
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 IsBalanceTree(Node* root)
{
if (nullptr == root)
return true;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
int diff = rightHeight - leftHeight;
if (abs(diff) >= 2)
{
cout << root->_kv.first << "高度差异常" << endl;
return false;
}
if (root->_bf != diff)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return IsBalanceTree(root->_left) && IsBalanceTree(root->_right);
}
2.6 AVL树的删除
删除操作我们可以进行与二叉搜索树类似的操作:
首先查找元素是否在二叉搜索树中,如果不存在,则返回false。
如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)
- 要删除结点N左右孩子均为空
- 要删除的结点N左孩子位空,右孩子结点不为空,那么删除节点的父节点则应该指向删除节点的右孩子
- 要删除的结点N右孩子位空,左孩子结点不为空,那么删除节点的父节点则应该指向删除节点的左孩子
- 要删除的结点N左右孩子结点均不为空
也就是替换法,替换完成后就完了吗?当然不是,对节点的删除必然会导致平衡因子的改变,因此我们需要调整
由于我们使用替代法,所以实际真删除的是被替代的那个节点,因此得从该节点的父节点开始调整
如果是0,说明从-1或者1调整过去,高度减少了1,需要向上调整
如果是-1或者1,最大高度不变,就相当于有两根绳子,一根不变,另一根少了一节,高度差从原来的0变成了-1或者1,最大长度变吗?不变,平衡因子看的是最大高度,所以不需要调整
如果是-2或者2,不平衡,直接旋转
cpp
bool Erase(const K& key)
{
// 空树情况
if (_root == nullptr)
return false;
Node* parent = nullptr;
Node* cur = _root;
// 查找要删除的节点
while (cur)
{
if (cur->_kv.first < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
parent = cur;
cur = cur->_left;
}
else
{
// 找到要删除的节点
break;
}
}
// 没有找到要删除的节点
if (cur == nullptr)
return false;
// 情况1:要删除的节点有两个子节点
if (cur->_left && cur->_right)
{
// 找到右子树的最小节点(后继)
Node* minParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minParent = minRight;
minRight = minRight->_left;
}
// 用后继节点的值替换当前节点的值
cur->_kv = minRight->_kv;
// 转换为删除后继节点(后继节点最多只有一个子节点)
cur = minRight;
parent = minParent;
}
// 情况2和3:要删除的节点是叶子节点或只有一个子节点
Node* child = nullptr;
if (cur->_left)
child = cur->_left;
else
child = cur->_right;
// 更新父节点指针
if (child)
child->_parent = parent;
// 更新根节点或父节点的子节点指针
if (parent == nullptr)
{
_root = child;
}
else
{
if (cur == parent->_left)
parent->_left = child;
else
parent->_right = child;
// 更新平衡因子
UpdateBalanceFactorAfterErase(parent, child);
}
delete cur;
return true;
}
void UpdateBalanceFactorAfterErase(Node* parent, Node* child)
{
Node* cur = parent;
while (cur)
{
// 更新平衡因子
if (child == cur->_left)
cur->_bf++;
else
cur->_bf--;
// 根据平衡因子决定下一步操作
if (cur->_bf == 1 || cur->_bf == -1)
{
// 高度不变,停止更新
break;
}
else if (cur->_bf == 0)
{
// 高度变化,继续向上更新
child = cur;
cur = cur->_parent;
}
else if (cur->_bf == 2 || cur->_bf == -2)
{
// 需要旋转
Node* higherChild = nullptr;
if (cur->_bf == 2)
{
higherChild = cur->_right;
if (higherChild->_bf == 0)
{
RotateL(cur);
cur->_bf = 1;
higherChild->_bf = -1;
break;
}
else if (higherChild->_bf == 1)
{
RotateL(cur);
}
else // higherChild->_bf == -1
{
RotateRL(cur);
}
}
else // cur->_bf == -2
{
higherChild = cur->_left;
if (higherChild->_bf == 0)
{
RotateR(cur);
cur->_bf = -1;
higherChild->_bf = 1;
break;
}
else if (higherChild->_bf == -1)
{
RotateR(cur);
}
else // higherChild->_bf == 1
{
RotateLR(cur);
}
}
// 旋转后继续向上更新
child = cur;
cur = cur->_parent;
}
}
}
总结
AVL树通过引入平衡因子和旋转操作,确保了二叉搜索树在任何情况下都能保持近似平衡的状态,从而保证了所有操作的时间复杂度都为O(log N)。虽然维护平衡需要额外的开销,但在需要频繁查找的场景下,AVL树提供了稳定的高性能保证。
插入操作的特点:
-
插入后最多只需要一次旋转即可恢复平衡
-
旋转后子树高度恢复原状,不会影响更高层节点
删除操作的特点:
-
删除后可能需要多次旋转
-
旋转后子树高度可能变化,需要继续向上更新平衡因子
-
实现比插入操作更复杂
AVL树的实现虽然相对复杂,但它为我们理解更高级的数据结构(如红黑树)奠定了基础。在实际应用中,我们可以根据具体需求选择是否使用AVL树------当查找操作远多于插入和删除操作时,AVL树是一个优秀的选择。