文章目录
-
- [1. 引言](#1. 引言)
- [2. AVL 树的概念](#2. AVL 树的概念)
-
- [2.1 平衡因子](#2.1 平衡因子)
- [2.2 为什么高度差不超过 1 而不是 0?](#2.2 为什么高度差不超过 1 而不是 0?)
- [3. AVL 树的实现](#3. AVL 树的实现)
-
- [3.1 AVL 树的结构](#3.1 AVL 树的结构)
- [3.2 AVL 树的插入](#3.2 AVL 树的插入)
-
- [3.2.1 插入一个值的大概过程](#3.2.1 插入一个值的大概过程)
- [3.2.2 平衡因子更新](#3.2.2 平衡因子更新)
- [3.2.3 插入结点及更新平衡因子的代码实现](#3.2.3 插入结点及更新平衡因子的代码实现)
- [4. 旋转](#4. 旋转)
- [5. AVL 树的查找](#5. AVL 树的查找)
- [6. AVL 树平衡检测](#6. AVL 树平衡检测)
1. 引言
AVL 树是最先发明的自平衡二叉查找树,由前苏联科学家 G. M. Adelson-Velsky 和 E. M. Landis 在 1962 年的论文《An algorithm for the organization of information》中提出。它通过控制左右子树的高度差来维持平衡,从而保证增删查改的效率稳定在 O(log N),相比普通二叉搜索树有了本质的提升。
今天我将从 AVL 树的核心概念出发,带你逐步理解平衡因子的更新、四种旋转操作,并给出完整的 C++ 实现代码。
2. AVL 树的概念
AVL 树是一颗空树,或者具备下列性质的二叉搜索树:
- 它的左右子树都是 AVL 树;
- 左右子树的高度差的绝对值不超过 1。
AVL 树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。

2.1 平衡因子
AVL 树实现中我们引入一个平衡因子 的概念。每个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度 ,也就是说任何结点的平衡因子等于 0/1/-1。
AVL 树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。
2.2 为什么高度差不超过 1 而不是 0?
思考一下为什么 AVL 树是高度平衡搜索二叉树,要求高度差不超过 1,而不是高度差是 0 呢?0 不是更好的平衡吗?
画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是 0 的。比如一棵树是 2 个结点、4 个结点等情况下,高度差最好就是 1,无法做到高度差是 0。
AVL 树整体结点数量和分布和完全二叉树类似,高度可以控制在 log N,那么增删查改的效率也可以控制在 O(log N),相比二叉搜索树有了本质的提升。
3. AVL 树的实现
3.1 AVL 树的结构
cpp
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:
//...
private:
Node* _root = nullptr;
};
3.2 AVL 树的插入
3.2.1 插入一个值的大概过程
-
插入一个值按二叉搜索树规则进行插入。
-
新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点到根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了。
-
更新平衡因子过程中没有出现问题,则插入结束。
-
更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。
3.2.2 平衡因子更新
更新原则:
-
平衡因子 = 右子树高度 - 左子树高度;
-
只有子树高度变化才会影响当前结点平衡因子;
-
插入结点会增加高度,所以新增结点在 parent 的右子树,parent 的平衡因子++,新增结点在 parent 的左子树,parent 平衡因子--;
-
parent 所在子树的高度是否变化决定了是否会继续往上更新。
更新停止条件:
- 更新后 parent 的平衡因子等于 0 ,更新中 parent 的平衡因子变化为 -1->0 或者 1->0,说明更新前 parent 子树一边高一边低,新增的结点插在低的那边,插入后 parent 所在的子树高度不变,不会影响 parent 的父亲结点的平衡因子,更新结束。
- 更新后 parent 的平衡因子等于 1 或 -1 ,更新前更新中 parent 的平衡因子变化为 0->1 或者 0->-1,说明更新前 parent 子树两边一样高,新增的插入结点后,parent 所在的子树一边高一边低,parent 所在的子树符合平衡要求,但是高度增加了 1,会影响 parent 的父亲结点的平衡因子,所以要继续向上更新。
- 更新后 parent 的平衡因子等于 2 或 -2 ,更新前更新中 parent 的平衡因子变化为 1->2 或者 -1->-2,说明更新前 parent 子树一边高一边低,新增的插入结点在高那边,parent 所在的子树高的那边更高了,破坏了平衡,parent 所在的子树不符合平衡要求,需要旋转处理。旋转的目标有两个:1、把 parent 子树旋转平衡;2、降低 parent 子树的高度,恢复到插入结点以前的高度。所以旋转后也不需要继续往上更新,插入结束。
- 不断更新,更新到根,根的平衡因子是 1 或 -1 也停止了。
更新到10结点,平衡因⼦为2,10所在的⼦树已经不平衡,需要旋转处理

更新到中间结点,3为根的⼦树⾼度不变,不会影响上⼀层,更新结束

最坏更新到根停⽌

3.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;
}
4. 旋转
4.1 旋转的原则
- 保持搜索树的规则;
- 让旋转的树从不满足变平衡 ,其次降低旋转树的高度。
旋转总共分为四种:左单旋 / 右单旋 / 左右双旋 / 右左双旋。
说明:下面的图中,有些结点我们给的是具体值,如 10 和 5 等结点,这里是为了方便讲解,实际中是什么值都可以,只要大小关系符合搜索树的性质即可。
4.2 右单旋
本图1展示的是 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 为根的树左边太高了,需要往右边旋转,控制两棵树的平衡。
旋转核心步骤:因为 5 < b 子树的值 < 10,将 b 变成 10 的左子树,10 变成 5 的右子树,5
变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的 h+2,符合旋转原则。如果插入之前 10
是整棵树的一个局部子树,旋转后不会再影响上一层,插入结束了。

a/b/c三棵树就提可以有下面几种情况:




右单旋代码实现
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;
// parent 有可能是整棵树的根,也可能是局部的子树
// 如果是整棵树的根,要修改 _root
// 如果是局部的指针要跟上一层链接
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;
}
4.3 左单旋
本图6展示的是 10 为根的树,有 a/b/c 抽象为三棵高度为 h 的子树(h>=0),a/b/c 均符合 AVL 树的要求。10 可能是整棵树的根,也可能是一个整棵树中局部的子树的根。
在 a 子树中插入一个新结点,导致 a 子树的高度从 h 变成 h+1,不断向上更新平衡因子,导致 10 的平衡因子从 1 变成 2,10 为根的树左右高度差超过 1,违反平衡规则。10 为根的树右边太高了,需要往左边旋转,控制两棵树的平衡。
旋转核心步骤:因为 10 < b 子树的值 < 15,将 b 变成 10 的右子树,10 变成 15 的左子树,15
变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的 h+2,符合旋转原则。

左单旋代码实现
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;
}
4.4 左右双旋
通过图 7 和图 8 可以看到,左边高时,如果插入位置不是在 a 子树,而是插入在 b 子树,b 子树高度从 h 变成 h+1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边高,但是插入在 b 子树中,10 为根的子树不再是单纯的左边高,对于 10 是左边高,但是对于 5 是右边高,需要用两次旋转才能解决,以 5 为旋转点进行一个左单旋,以 10 为旋转点进行一个右单旋,这棵树就平衡了。


下面我们将 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。
- 场景 3:h == 0 时,a/b/c 都是空树,b 自己就是一个新增结点,不断更新 5->10 平衡因子,引发旋转,其中 8 的平衡因子为 0,旋转后 8 和 10 和 5 平衡因子均为 0。

左右双旋代码实现
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);
}
}
4.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:h >= 1 时,新增结点插入在 f 子树,f 子树高度从 h-1 变为 h 并不断更新 12->15->10 平衡因子,引发旋转,其中 12 的平衡因子为 1,旋转后 15 和 12 平衡因子为 0,10 平衡因子为 -1。
- 场景 3:h == 0 时,a/b/c 都是空树,b 自己就是一个新增结点,不断更新 15->10 平衡因子,引发旋转,其中 12 的平衡因子为 0,旋转后 10 和 12 和 15 平衡因子均为 0。

右左双旋代码实现
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);
}
}
5. AVL 树的查找
按二叉搜索树逻辑实现即可,搜索效率为 O(log N)。
cpp
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;
}
6. AVL 树平衡检测
我们实现的 AVL 树是否合格,我们通过检查左右子树高度差的程序进行反向验证,同时检查一下结点的平衡因子更新是否出现了问题。
cpp
1 int _Height(Node* root)
2 {
3 if (root == nullptr)
4 return 0;
5
6 int leftHeight = _Height(root->_left);
7 int rightHeight = _Height(root->_right);
8
9 return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
10 }
11
12 bool _IsBalanceTree(Node* root)
13 {
14 // 空树也是AVL树
15 if (nullptr == root)
16 return true;
17
18 // 计算pRoot结点的平衡因⼦:即pRoot左右⼦树的⾼度差
19 int leftHeight = _Height(root->_left);
20 int rightHeight = _Height(root->_right);
21 int diff = rightHeight - leftHeight;
22
23 // 如果计算出的平衡因⼦与pRoot的平衡因⼦不相等,或者
24 // pRoot平衡因⼦的绝对值超过1,则⼀定不是AVL树
25 if (abs(diff) >= 2)
26 {
27 cout << root->_kv.first << "⾼度差异常" << endl;
28 return false;
29 }
30
31 if (root->_bf != diff)
32 {
33 cout << root->_kv.first << "平衡因⼦异常" << endl;
34 return false;
35 }
36
37 // pRoot的左和右如果都是AVL树,则该树⼀定是AVL树
38 return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
39 }
40
41 // 测试代码
42 void TestAVLTree1()
43 {
44 AVLTree<int, int> t;
45 // 常规的测试⽤例
46 //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
47 // 特殊的带有双旋场景的测试⽤例
48 int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
49 for (auto e : a)
50 {
51 t.Insert({ e, e });
52 }
53
54 t.InOrder();
55 cout << t.IsBalanceTree() << endl;
56 }
57
58 // 插⼊⼀堆随机值,测试平衡,顺便测试⼀下⾼度和性能等
59 void TestAVLTree2()
60 {
61 const int N = 100000;
62 vector<int> v;
63 v.reserve(N);
64 srand(time(0));
65
66 for (size_t i = 0; i < N; i++)
67 {
68 v.push_back(rand()+i);
69 }
70
71 size_t begin2 = clock();
72 AVLTree<int, int> t;
73 for (auto e : v)
74 {
75 t.Insert(make_pair(e, e));
76 }
77 size_t end2 = clock();
78
79 cout << "Insert:" << end2 - begin2 << endl;
80 cout << t.IsBalanceTree() << endl;
81
82 cout << "Height:" << t.Height() << endl;
83 cout << "Size:" << t.Size() << endl;
84
85 size_t begin1 = clock();
86 // 确定在的值
87 /*for (auto e : v)
88 {
89 t.Find(e);
90 }*/
91
92 // 随机值
93 for (size_t i = 0; i < N; i++)
94 {
95 t.Find((rand() + i));
96 }
97
98 size_t end1 = clock();
99
100 cout << "Find:" << end1 - begin1 << endl;
101 }