1. AVL树的概念
AVL 树是 最先发明的自平衡二叉搜索树 ,AVL 是一颗空树,或者具备下列性质的二叉搜索树:它的左右子树都是 AVL 树,且左右子树的高度差的绝对值不超过 1 。AVL 树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。所以说,AVL 树就是一种特殊的二叉搜索树。
AVL 树得名于它的发明者 G. M. Adelson-Velsky 和 E. M. Landis 是两个前苏联的科学家,他们在 1962 年的论文《An algorithm for the organization of information》中发表了它。
AVL 树实现这里我们引入一个平衡因子 (balance factor) 的概念,每个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度 ,也就是说任何结点的平衡因子等于 0/1/-1,AVL 树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。
思考一下为什么 AVL 树是高度平衡搜索二叉树,要求高度差不超过 1,而不是高度差是 0 呢?0 不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是 0 的。比如一棵树是 2 个结点,4 个结点等情况下,高度差最好就是 1,无法做到高度差是 0。
AVL 树整体结点数量与分布和完全二叉树类似,高度可以控制在logN,那么增删查改的效率也可以控制在O(logN),相比二叉搜索树有了本质的提升。

比如这就是一个AVL树,其节点上的数字就是每个节点的平衡因子。
2. AVL树的实现
2.1 AVL树的结构

AVL树因为是一个特殊的二叉搜索树,所以结构和二叉搜索树是高度类似的,不过这里还多了一个平衡因子的变量以及一个_parent的指针,这个指针的作用后续会提及。
2.2 AVL树的插入
既然AVL树是特殊的二叉搜索树,那么对于插入函数来说,整体的框架是没有大的变化的:
cpp
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(key,value);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key,value);
cur->_bf = 0;
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
return true;
}
在这里我们只是控制了_parent指针的指向以及_bf的值。最主要的是下一步要做的:如何实现控制平衡。
2.2.1 插入的过程
新增结点以后,可能会影响祖先结点的高度,所谓祖先节点,就是从插入的这个节点的父节点开始到根节点之间的路径中的节点,也就是可能会影响部分祖先结点的平衡因子,所以需要更新从新增结点 -> 根结点路径上的节点的平衡因子。实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了。

就像这样,在原先的AVL树种插入一个节点 9 ,那么 9 的祖先节点中的节点 10 的平衡因子需要改变,因为新插入的节点影响了其父节点的左右子树高度差,但是对根节点的左右子树高度差没有影响。
2.2.2 平衡因子的更新
并且我们知道:平衡因子 = 右子树高度 - 左子树高度。也就意味着**只有子树高度变化才会影响当前结点平衡因子。**插入结点,会增加高度,所以新增结点在 parent 的右子树,parent 的平衡因子 ++,新增结点在 parent 的左子树,parent 平衡因子 -- 。
并且我们更新平衡因子是要沿着插入节点的祖先节点的这个路径往上去更新,就如上图中,插入的是 9 ,那么 10 和 8 就是 9 的祖先节点。那么现在有一个问题,我们怎么才能知道什么时候是可以不用更新平衡因子,什么时候要继续向上更新平衡因子呢?这里的关键就在于:parent 所在子树的高度是否变化。这会有三种情况:

第一种:更新后parent平衡因子为 0
我们以这幅图为例,我们可以发现,只要是插入一个数据,更新后 parent 的平衡因子就等于 0,更新中 parent 的平衡因子变化为 - 1->0 或者 1->0,说明更新前 parent 子树一边高一边低,新增的结点一定是插入在低的那边,插入后 parent 所在的子树高度不变,不会影响 parent 的父亲结点的平衡因子,更新结束。
第二种:更新后parent平衡因子为1或-1
更新后 parent 的平衡因子等于 1 或 - 1,更新前更新中 parent 的平衡因子变化为 0->1 或者 0->-1,说明更新前 parent 子树两边一样高,新增的插入结点后,parent 所在的子树一边高一边低,parent 所在的子树符合平衡要求,但是高度增加了 1,会影响 parent 的父亲结点的平衡因子,所以要继续向上更新。

第三种:更新后parent平衡因子为2或-2
更新后 parent 的平衡因子等于 2 或 - 2,更新中 parent 的平衡因子变化为 1->2 或者 - 1->-2,说明更新前 parent 子树一边高一边低,新增的插入结点在高的那边:

此时 parent 所在的子树高的那边更高了,破坏了平衡,parent 所在的子树不符合平衡要求,需要旋转处理,旋转的目标有两个:1、把 parent 子树旋转平衡。2、降低 parent 子树的高度,恢复到插入结点以前的高度。所以旋转后也不需要继续往上更新,插入结束。
最后不断更新,更新到根,根的平衡因子是 1 或 - 1 也停止了。
那我们更新平衡因子的代码可以先这样写:

最后一个else语句中的assert(false)是为了应对,在插入这个数据之前,这棵树本身就不是一个AVL树,那就可以直接利用assert断言报错。另外while循环的判断条件是parent不等于nullptr,这是因为最坏的结果是一路更新到根节点,那么此时parent就是根节点,根据parent = parent->_parent;这句代码,parent就会被置为nullptr,至此循环结束。
2.3 旋转
2.3.1 旋转的原则
-
保持搜索树的规则
-
让旋转的树从不平衡变平衡,其次降低旋转树的高度
旋转总共分为四种,左单旋 / 右单旋 / 左右双旋 / 右左双旋。
说明:下面的图中,有些结点我们给的是具体值,如 10 和 5 等结点,这里是为了方便讲解,实际中是什么值都可以,只要大小关系符合搜索树的性质即可。
2.3.2 右单旋
本图 1 展示的是 10 为根的树,有 a/b/c 抽象为三棵高度为 h 的子树 (h>=0),a/b/c 均符合 AVL 树的要求。**这里的subL代表左子树,subLR代表右子树。10 可能是整棵树的根,也可能是一个整棵树中局部的子树的根。**这里 a/b/c 是高度为 h 的子树,是一种概括抽象表示,他代表了所有右单旋的场景。实际右单旋形态有很多种,具体图 2 / 图 3 / 图 4 / 图 5 进行了详细描述。
在 a 子树中插入一个新结点,导致 a 子树的高度从 h 变成 h+1,不断向上更新平衡因子,导致 10 的平衡因子从 - 1 变成 - 2,10 为根的树左右高度差超过 1,违反平衡规则。10 为根的树左边太高了,需要往右边旋转,控制两棵树的平衡。
旋转核心步骤,因为 5 < b 子树的值 < 10,将 b 变成 10 的左子树,10 变成 5 的右子树,5 变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的 h+2,符合旋转原则。如果插入之前 10 整棵树的一个局部子树,旋转后不会再影响上一层,插入结束了。
因此所谓的右单旋,就是左子树的高度太高了,需要向右边旋转以达到平衡树的目的。

那为什么叫右单旋呢,通过这张图大家可能好理解一些:

就相当于把10这个节点,按照顺时针的顺序往下按,图中箭头是指向右,所以叫右单旋。
下面展示右单旋的不同情况,这边举例的情况都在一个前提下:a这颗子树插入之后,自身不会发生旋转:




图2-5都是对图1的一个实例化演示,目的时为了能让大家更好的去理解图1,现在我们来尝试编写右单旋的代码,用刚刚讲的insert插入函数举例 :

发生旋转场景时,根节点的平衡因子不是2就是-2,当该旋转场景是右单旋时,其根节点和根节点的左子节点的平衡因子分别是:-2和-1。在这里我们用一个函数RotateR来实现旋转的目的,Rotate的意思就是旋转,其中最后一个字母R代表Right,通过上面的图片我们可以得知,我们所需要调整的节点就只有:根节点、根节点的左子节点、根节点的左子节点的右子节点,分别命名为parent、subL、subLR。
另外,我们除了要调整这三个节点位置关系,还要改变这三个节点之间的父子关系。并且对于subL的父节点需要有特殊处理,因为旋转的有可能是局部子树,那subL被转化成根节点之后,它的父节点就需要做出调整,所以还要先记录下原来的旋转点的父节点,然后再进一步讨论:

位置关系和父子关系修改完成之后,再把subL和parent的平衡因子修改一下即可。
2.3.3 左单旋
本图 6 展示的是 10 为根的树,有 a/b/c 抽象为三棵高度为 h 的子树 (h>=0),a/b/c 均符合 AVL 树的要求。10 可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里 a/b/c 是高度为 h 的子树,是一种概括抽象表示,他代表了所有左单旋的场景,具体跟上面左旋类似。
在 a 子树中插入一个新结点,导致 a 子树的高度从 h 变成 h+1,不断向上更新平衡因子,导致 10 的平衡因子从 1 变成 2,10 为根的树左右高度差超过 1,违反平衡规则。10 为根的树右边太高了,需要往左边旋转,控制两棵树的平衡。
旋转核心步骤,因为 10 < b 子树的值 < 15,将 b 变成 10 的右子树,10 变成 15 的左子树,15 变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的 h+2,符合旋转原则。如果插入之前 10 整棵树的一个局部子树,旋转后不会再影响上一层,插入结束了。

可以看到原理和上面讲的右单旋几乎一模一样,仅仅是旋转点要旋转的方向不同而已。我们就直接来编写代码:

2.3.4 左右双旋
通过图 7 和图 8 可以看到,左边高时,如果插入位置不是在 a 子树,而是插入在 b 子树,b 子树高度从 h 变成 h+1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的是纯粹的左边高,但是插入在 b 子树中,10 为根的子树不再是单纯的左边高,对于 10 是左边高,但是对于 5 是右边高,需要用两次旋转才能解决,以 5 为旋转点进行一个左单旋,以 10 为旋转点进行一个右单旋,这棵树这棵树就平衡了。
总的来说就是:因为右边高要进行左单旋,左边高要进行右单旋。在这里对于10是左边高,所以要右单旋;对于5来说是右边高,所以要左单旋。并且是先左单旋再右单旋,因此叫做左右双旋。


图 7 和图 8 分别为左右双旋中 h==0 和 h==1 具体场景,就是说这样的场景下,仅仅使用右单旋或者左单旋,没有办法解决不平衡的问题。
下面我们将 a/b/c 子树抽象为高度 h 的 AVL 子树进行分析,另外我们需要把 b 子树的细节进一步展开为 8 和左子树高度为 h-1 的 e 和 f 子树,因为我们要对 b 的父亲 5 为旋转点进行左单旋**,大家可以再到上面看一下左单旋的场景示意**,这里左单旋需要动 b 树中的左子树。

b 子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察 8 的平衡因子不同,这里我们要分三个场景讨论。
场景 1:h>=1 时,新增结点插入在 e 子树,e 子树高度从 h-1 并为 h 并不断更新 8->5->10 平衡因子,引发旋转,其中 8 的平衡因子为 - 1,旋转后 8 和 5 平衡因子为 0,10 平衡因子为 1。

场景 2:h>=1 时,新增结点插入在 f 子树,f 子树高度从 h-1 变为 h 并不断更新 8->5->10 平衡因子,引发旋转,其中 8 的平衡因子为 1,旋转后 8 和 10 平衡因子为 0,5 平衡因子为 - 1。

在这里大家可以发现一个共性:这里左右双旋的本质,实际上是把b子树的根节点的左右子树,分别交给subL和parent两个节点,并且e对应subL的右子节点,f对应parent的左子节点,然后subL和parent分别变为subLR的左右子节点 ,这个位置关系是不会改变的。而之所以要分成两个场景,是因为插入的节点在e后面还是f后面,会影响到subL和parent这两个节点的平衡因子。
场景 3:h==0 时,a/b/c 都是空树,b 自己就是一个新增结点,不断更新 5->10 平衡因子,引发旋转,其中 8 的平衡因子为 0,旋转后 8 和 10 和 5 平衡因子均为 0。

最后需要注意的是,区分这三个场景的最关键特征,就是subLR这个节点的平衡因子的值,如果是 -1 ,就代表第一个场景;如果是 1 ,就代表第二个场景;如果是 0 ,就代表第三个场景。
现在来编写代码:

因为是对subL进行左单旋,对parent进行右单旋,所以这里直接复用了左单旋和右单旋的代码,因此位置关系得到了解决。那接下来去改正平衡因子,因为要改正平衡因子得确定旋转发生之后对应哪个场景,关键点在于subLR的在位置关系改变之前的平衡因子,所以我们先用 bf 变量存储下来。
最后展现完整代码:

2.3.5 右左双旋
跟左右双旋类似,下面我们将 a/b/c 子树抽象为高度 h 的 AVL 子树进行分析,另外我们需要把 b 子树的细节进一步展开为 12 和左子树高度为 h-1 的 e 和 f 子树,因为我们要对 b 的父亲 15 为旋转点进行右单旋,右单旋需要动 b 树中的右子树。b 子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察 12 的平衡因子不同,这里我们要分三个场景讨论。

但是因为和左右双旋的高度相似性,在这里我只展示第一种场景:
场景 1:h>=1 时,新增结点插入在 e 子树,e 子树高度从 h-1 变为 h 并不断更新 12->15->10 平衡因子,引发旋转,其中 12 的平衡因子为 - 1,旋转后 10 和 12 平衡因子为 0,15 平衡因子为 1。

代码的实现逻辑和左右双旋也是非常相似的,所以我就直接展示代码:

2.4 AVL树的平衡检测
插入操作实现完成之后,因为在插入的过程当中可能对于一些参数比如平衡因子等等会出现插入错误,但最后结果正确,所以我们还需要检测我们实现的AVL树是否是一个合格的二叉平衡搜索树。
检查的方式就是求出每个根节点的左右子树高度,求出差值之后再和当前的平衡因子作比较,如果相等,那就合格,否则就不合格。

这里使用的是递归的方法求解树的高度,思路非常巧妙,以这个树举例:

那代码逻辑就是这样的:


然后是检测AVL树的代码:

这里先求出左右子树的高度,然后相减得到差值。在这里用到了 abs 函数,它的作用是求出该参数的绝对值,在这里如果左右子树的高度差的绝对值大于等于 2 或者高度差不等于当前节点的平衡因子,就证明树有问题。该节点检查完了之后,再利用递归去检查这棵树的左子树和右子树。

另外,我们这里还给出了求实际插入了多少个节点的函数,这里使用的也是递归。
最后展示一下全部代码:
cpp
#pragma once
#include<assert.h>
template<class K, class V>
struct AVLTreeNode
{
// 需要parent指针,后续更新平衡因⼦可以看到
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)
{}
};
template<class K , class V>
class AVLTree
{
typedef AVLTreeNode<K,V> Node;
public:
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 (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);
cur->_bf = 0;
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
while (parent)
{
if (cur == parent->_right)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
if (parent->_bf == 0)
{
break; //父节点的平衡因子更新后为0,则更新结束
}
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);
}
break;
}
else
{
assert(false);
}
}
return true;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
bool IsBalanceTree()
{
return _IsBalanceTree(_root);
}
int Height()
{
return _Height(_root);
}
int Size()
{
return _Size(_root);
}
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
{
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << " ";
//cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
int _Size(Node* root)
{
return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
}
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 (root == nullptr)
return true;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
int bf = rightHeight - leftHeight;
if (abs(bf) >= 2 || bf != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return _IsBalanceTree(root->_left)
&& _IsBalanceTree(root->_right);
}
private:
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR; //改变subL、subLR、parent之间的位置关系
if (subLR != nullptr)
{
subLR->_parent = parent;
}
//改变三个节点的父子关系
//先记录下原来的旋转点的父节点
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
//下面改变subL的父节点
if (parent == _root) //旋转点是根节点
{
_root = subL;
subL->_parent = nullptr;
}
else //旋转点是一个局部子树
{
if (parentParent->_left == parent)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
subL->_bf = parent->_bf = 0;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//先改变位置关系
parent->_right = subRL;
subR->_left = parent;
//改变父子关系
Node* parentParent = parent->_parent;
parent->_parent = subR;
if (subRL != nullptr)
{
subRL->_parent = parent;
}
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;
}
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;
subLR->_bf = 0;
parent->_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);
}
}
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(subR);
RotateL(parent);
if (bf == 0) //对应场景三
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_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);
}
}
private:
Node* _root = nullptr;
};
本文到此结束,感谢各位读者的阅读,如果有讲解的不到位或者错误的地方,欢迎各位读者批评和指正。