
关注我,学习c++不迷路:
专栏如下:
后续会更新更多有趣的小知识,关注我带你遨游知识世界

期待你的关注。

文章目录
- [1. 什么是AVL树?](#1. 什么是AVL树?)
- [2. AVL的实现:](#2. AVL的实现:)
-
- [2-1 底层结构:](#2-1 底层结构:)
- 2-2AVL的插入
- [2-3 其余函数:](#2-3 其余函数:)
- [4 测试:](#4 测试:)
- [3. 总结:](#3. 总结:)
1. 什么是AVL树?
再之前写的搜索二叉树中,由于数据小于父亲节点,会导致形成一条高度比较高,此时进行检索或者插入时,时间复杂度过高。在普通二叉搜索树中,如果插入的数据是有序的(例如连续插入 1, 2, 3, 4, 5),树会退化成一条链表,使得查找、插入、删除操作的时间复杂度从理想的 O(log n) 恶化到 O(n)。AVL 树是最早被发明的自平衡二叉查找树。它的名字来源于其发明者 G. M. Adelson-Velsky 和 E. M. Landis。
与二叉搜索树不同的时我们引入了平衡因子来控制树的高度差,这里着重讲一下我们的bf(balance fator)主要是右减去左。为了满足要求,我们要求平衡因子的范围在-1 0 1这个数字内。
同时为了便于AVL树的操作,我们还是引入了父亲节点这一结构。
2. AVL的实现:
2-1 底层结构:
这个结构和二叉搜索树的结构很是相似,我们先定义出他的节点:一个节点包含
- 本身的值,这个值可以使用pair来代替。pair中first来存储key的值,而second来存储val的值。检查key-val结构。
- 还包含一个bf这个int型变量,来记录该节点右子树减去左子树的高度。
- 最后包含三个方向节点指针,分别是指向左子树的
_left和右子树_right以及最后指向父亲节点的_parent。
代码如下,还是使用类模板,注意使用struct,对AVLTree做公开:
cpp
template <class K,class V>
struct AVLNode{
AVLNode(const pair<K,V> kv)
:_kv(kv)
,_bf(0)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
{ }
pair<K,V> _kv;//存储数据的关键
int _bf;//平衡因子:右减去左
AVLNode<K,V>* _left;
AVLNode<K,V>* _right;
AVLNode<K,V>* _parent;
};
这里一并完成了对其的初始化,对于bf来说,一开始给0是正常的,原因是:左右子树都指向空,那么平衡因子都是0.
随后定义一个AVLTree,完成对AVL树的结构的设计:
cpp
template <class K,class V>
class AVLTree {
using Node = AVLNode<K, V>;
public:
Node* _root = nullptr;
};
2-2AVL的插入
完成了上面的对结构设计,我们便可以尝试对AVL进行插入。但是插入一个数,便会导致被插入位置的父亲节点的平衡因子发生改变,如果不满足AVL树的结构,我们应该怎么办呢?
答案是旋转。那么什么样情况要对应什么样的旋转呢?我们先来平衡因子该如何进行更新:(bf 为右边树的高度减去左边的高度)
- 如果cur(要插入的节点)在parent的做左边,那么parent的
_bf--。 - 如果在右边,那么parent的
_bf++。
但是我们发现只更新这一个节点是不够的,我们在底下的插入也影响上面的平衡因子。因此要进行循环式更新:在循环内部cur变成parent ,parent变成parent的_parent。同时又有下面情况:
- 如果该parent的_bf == 0,则代表不需要更新,插入的该树只改变了这一父亲节点,不影响这一分支树的高度。高度是由最高的的高度的分支决定的。
- 如果该parent的_bf == -2/2,则代表不需要往后更新,此时这一部分已经不满足AVL树的结构。只需进行旋转进行调整。
- 如果该树的parent == -1/1,则进行往上进行更新,这是_bf变成-1 只有从0 变成-1 ,说明的树高度发生变化,同理变成 1也是如此。cur变成parent ,parent变成parent的_parent。
因此有以下的逻辑:
- 先进行找到需要插入的位置:
cpp
bool insert(const pair<K, V> kv)
{
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;//相等则表示插入不进入
}
cur = new Node(kv);
if (kv.first > parent->_kv.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
//开始检查树中的bf。
此时已经插入成功了,完成了插入这一个要求。但是平衡因子还没有进行更新,此时还需要进行更新:
cpp
//开始检查树中的bf。
while (parent)
{
if (cur == parent->_left)
parent->_bf--;
if (cur == parent->_right)
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)
{
//已经开始不平衡了,开始尝试旋转
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("No bf");
}
}
else {
assert("错误");
}
}
return true;
}
这就是AVL树的大体逻辑。我们接下来讲:什么是右旋和左旋还有双旋:
2-2-1:左旋和右旋:
先说右旋:这个是左边纯粹的高导致的,我们需要将左边的树旋转到右边去。比如下图:

此时需要往右边旋转,这个是抽象图,我们再看一个简单的例子:

此时这个情况比简单,我们先来说说:我只需要向下面的图片一样完成转化即可:

我们再来看抽象的情况:

先把subLR给p的左子树,同时让subL作为这个的parent的父亲,这样就完成了,但是他还有其他的链接,所以代码操作还是很麻烦的。这样我们就做到了AVL的平衡。完成了旋转。那么代码应该怎么怎么写呢?
cpp
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* Pparent = parent->_parent;
//开始旋转:
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
parent->_parent = subL;
if (Pparent == nullptr)
{
//代表这个parent 就是根
_root = subL;
subL->_parent = nullptr;
}
else {
if (Pparent->_left == parent)
{
Pparent->_left = subL;
subL->_parent = Pparent;
}
else {
Pparent->_right = subL;
subL->_parent = Pparent;
}
}
//最后更新平衡因子
parent->_bf = 0;
subL->_bf = 0;
}
这个代码在插入的时候还要更新parent,同时还要注意subLR是否为空,不是空才进行插入,还要注意这个部分是祖父的右子树还是左子树,都要进行判断,才完成链接。
同理右旋也是如此:

这边看图就可以知道是右边高,所以需要左旋,往左边旋转。当你不会写代码的时候就可以来画画图片就可以知道怎么写了:
cpp
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* Pparent = parent->_parent;
//开始旋转:
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
parent->_parent = subR;
if (Pparent == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else {
if (parent == Pparent->_left)
{
Pparent->_left = subR;
subR->_parent = Pparent;
}
else
{
Pparent->_right = subR;
subR->_parent = Pparent;
}
}
//更新平衡因子:
parent->_bf = 0;
subR->_bf = 0;
}
同时注意这两个旋转都会导致平衡因子变成0,可以看图就知道了。
2-2-2:双旋:
这里面就比较麻烦了:
这里是由于不是统一或者纯粹的高而导致的。
比如说左右旋转:先进行左旋解决局部右边高,在进行右旋,解决左边高的问题,看图:

我们来看抽象图:

完成详细的图片,那么这两部的操作的比较简单,难的就是如何更新平衡因子:

那么这样就好完成了就有:
先定义bf是subRL的_bf,后面的情况都是他来决定的:
cpp
int bf = subLR->_bf;
第一种情况:
cpp
if (bf == -1)
{
parent->_bf = 1;
subLR->_bf = 0;
subL->_bf = 0;
}
第二种情况:
cpp
else if (bf == 1)
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
第三章情况:
cpp
else if (bf == 0)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else {
assert("LR error");
}
这三种情况都是可以看图看出来的,如果subRL是平衡的,那么三个地方都是0,如果是-1,那么说明是右边矮了点,那么会导致subR的平衡因子是1.(做subR的左子树)。如果是1,那么同理,会导致p的_bf为-1.
代码如下:
cpp
void RotateLR(Node* parent)
{
//先左旋后右旋:
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
//开始更新平衡因子:
if (bf == -1)
{
parent->_bf = 1;
subLR->_bf = 0;
subL->_bf = 0;
}
else if (bf == 1)
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else if (bf == 0)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else {
assert("LR error");
}
}
cpp
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
//开始更新平衡因子
if (bf == 1)
{
subRL->_bf = 0;
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subRL->_bf = 0;
parent->_bf = 0;
subR->_bf = 1;
}
else if (bf == 0)
{
subRL->_bf = 0;
parent->_bf = 0;
subR->_bf = 0;
}
else {
assert("RL error");
}
}
2-3 其余函数:
中序遍历函数,这个之前讲过,直接给出代码:
cpp
void Inorder()
{
_Inorder(_root);
}
private:
void _Inorder(Node* root)
{
if (root == nullptr)
{
return;
}
//左 中 右
_Inorder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_Inorder(root->_right);
}
还要一个测试树的高度的函数:
cpp
int _Height(Node* root)
{
if (root == nullptr)
{
return 0;
}
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return 1 + (leftHeight > rightHeight ? leftHeight : rightHeight);
}
Node* _root = nullptr;
int Height()
{
//查询树的高度
return _Height(_root);
}
4 测试:
我们完成大致AVL的树构建,那么我们便可以进行尝试测试:
cpp
#include"AVLTree.h"
void TestAVLTree1()
{
AVLTree<int, int> t;
// 常规的测试用例
//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
// 特殊的带有双旋场景的测试用例
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a)
{
t.insert({ e, e });
}
t.Inorder();
}
int main()
{
TestAVLTree1();
return 0;
}

可以看到没有太多的问题。
3. 总结:
学习AVL树时,建议首先牢固掌握二叉搜索树的基本操作,再深入理解平衡因子的概念和四种旋转操作的本质。学习过程中应多动手绘图,模拟插入和删除节点时平衡因子的变化及触发旋转的各种情况,重点把握"L和R用单旋、LR和RL用双旋"的规律。理解比记忆更重要,要明白每种旋转都是为了降低子树高度差同时保持搜索树性质。初期可通过分步练习掌握每种旋转的指针调整步骤,再逐步过渡到完整实现。建议对照代码和图示分析,注意旋转后平衡因子的更新。最后通过综合练习,从简单到复杂反复操作,体会AVL树的自平衡过程,并与红黑树等其他平衡树对比,理解AVL树严格平衡的特性及其适用场景。坚持理论与实践结合,才能真正内化这一重要数据结构。