AVL树:从平衡因子到四种旋转的完整实现
文章目录
- AVL树:从平衡因子到四种旋转的完整实现
-
- [1 AVL树的基本概念](#1 AVL树的基本概念)
- [2 为什么要求高度差不超过1](#2 为什么要求高度差不超过1)
- [3 平衡因子 Balance Factor](#3 平衡因子 Balance Factor)
- [4 AVL树的结点结构](#4 AVL树的结点结构)
- [5 AVL树的整体结构](#5 AVL树的整体结构)
- [6 AVL树的插入](#6 AVL树的插入)
- [7 AVL树插入的基本过程](#7 AVL树插入的基本过程)
- [8 平衡因子的更新规则](#8 平衡因子的更新规则)
- [9 更新平衡因子的三种情况](#9 更新平衡因子的三种情况)
-
- [9.1 更新后变成0](#9.1 更新后变成0)
- [9.2 更新后变成1或-1](#9.2 更新后变成1或-1)
- [9.3 更新后变成2或-2](#9.3 更新后变成2或-2)
- [10 AVL插入代码](#10 AVL插入代码)
- [11 AVL树的旋转](#11 AVL树的旋转)
- [12 右单旋](#12 右单旋)
- [13 右单旋的核心](#13 右单旋的核心)
- [14 右单旋代码](#14 右单旋代码)
- [15 右旋过程中为什么需要处理subLR](#15 右旋过程中为什么需要处理subLR)
- [16 左单旋](#16 左单旋)
- [17 左单旋代码](#17 左单旋代码)
- [18 左右双旋](#18 左右双旋)
- [19 左右双旋的结构变化](#19 左右双旋的结构变化)
- [20 左右双旋为什么需要特殊处理平衡因子](#20 左右双旋为什么需要特殊处理平衡因子)
- [21 左右双旋代码](#21 左右双旋代码)
- [22 右左双旋](#22 右左双旋)
- [23 右左双旋代码](#23 右左双旋代码)
- [24 四种旋转如何判断](#24 四种旋转如何判断)
- [25 AVL树查找](#25 AVL树查找)
- [26 AVL树的平衡检测](#26 AVL树的平衡检测)
- [27 计算树的高度](#27 计算树的高度)
- [28 判断是否为AVL树](#28 判断是否为AVL树)
- [29 AVL树平衡检测代码](#29 AVL树平衡检测代码)
- [30 AVL树的测试](#30 AVL树的测试)
- [31 大规模数据测试](#31 大规模数据测试)
- [32 AVL树的核心代码逻辑](#32 AVL树的核心代码逻辑)
1 AVL树的基本概念
AVL树是一种自平衡二叉搜索树
普通二叉搜索树在插入数据后,如果数据本身具有某种顺序,例如依次插入 1 2 3 4 5,很容易退化成类似链表的结构
此时搜索效率会从理想情况下的 O(logN) 退化到 O(N)
AVL树通过控制树的高度,使二叉搜索树始终保持较好的平衡状态,从而保证查找等操作的效率
AVL树的定义
一棵树满足下面几个条件,就可以认为它是一棵 AVL 树
1 这棵树是一棵空树,或者是一棵二叉搜索树
2 左右子树本身也必须是 AVL 树
3 任意结点的左右子树高度差的绝对值不能超过 1
也就是
cpp
abs(左子树高度 - 右子树高度) <= 1
因此 AVL 树本质上是一棵高度平衡的二叉搜索树
2 为什么要求高度差不超过1
很多人第一次接触 AVL 树时会产生一个问题
既然是平衡树,为什么不直接要求左右子树高度完全相等呢
也就是为什么不要求
cpp
左子树高度 == 右子树高度
原因是这种要求过于严格,有些结点数量下根本无法做到
例如一棵树只有两个结点
text
1
\
2
此时左右子树高度必然存在差异
因此 AVL 树采用的是更加合理的要求
text
左右子树高度差 <= 1
这样既可以保证树不会严重倾斜,又不会对树的结构要求过于苛刻
3 平衡因子 Balance Factor
为了方便判断 AVL 树是否平衡,给每一个结点增加一个平衡因子 _bf
平衡因子的定义是
cpp
平衡因子 = 右子树高度 - 左子树高度
因此
text
_bf = rightHeight - leftHeight
对于正常的 AVL 树来说,每个结点的平衡因子只能是
text
-1
0
1
如果出现
text
2
说明右边太高
如果出现
text
-2
说明左边太高
例如
text
10
/
5
左边高度比右边高 1
所以
text
_bf = 0 - 1 = -1
再例如
text
10
\
15
右边高度比左边高 1
所以
text
_bf = 1 - 0 = 1
如果变成
text
10
/
5
/
3
那么
text
左子树高度 = 2
右子树高度 = 0
_bf = 0 - 2 = -2
此时就出现了不平衡,需要进行旋转
4 AVL树的结点结构
AVL树的结点不仅需要保存左右孩子,还需要保存父亲结点以及平衡因子
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;
AVLTreeNode(const pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
各成员的作用
| 成员 | 作用 |
|---|---|
_kv |
保存键值对 |
_left |
指向左孩子 |
_right |
指向右孩子 |
_parent |
指向父亲结点 |
_bf |
保存平衡因子 |
这里的 _parent 非常重要
因为插入一个新结点之后,需要从新结点的父亲开始一路向上更新平衡因子
如果没有 _parent,就无法直接从当前结点找到上一层结点
5 AVL树的整体结构
cpp
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
private:
Node* _root = nullptr;
};
_root 保存整棵 AVL 树的根结点
6 AVL树的插入
AVL树插入一个结点的过程,可以分成几个阶段
text
按照二叉搜索树规则插入
↓
更新祖先结点的平衡因子
↓
判断是否出现不平衡
↓
如果平衡,继续向上更新
↓
如果不平衡,进行旋转
↓
旋转完成后结束
最关键的一点是
AVL树的插入首先仍然遵循二叉搜索树的插入规则
AVL树并没有改变二叉搜索树的基本性质
7 AVL树插入的基本过程
假设现在插入一个新结点
首先按照普通二叉搜索树的方法寻找插入位置
text
如果 key < 当前结点
向左走
如果 key > 当前结点
向右走
如果 key == 当前结点
插入失败
新结点插入以后,需要开始向上更新平衡因子
更新路径为
text
新结点
↑
parent
↑
parent
↑
...
↑
root
最坏情况下需要一直更新到根结点
但并不是每次都需要更新到根
有些情况下更新到中间位置就可以停止
8 平衡因子的更新规则
AVL树中平衡因子的定义是
cpp
_bf = 右子树高度 - 左子树高度
插入一个新结点后,新结点所在的子树高度可能增加
因此父亲结点的平衡因子可能发生变化
插入到左边
如果新结点插入到 parent 的左子树
cpp
parent->_bf--;
因为左子树高度增加了
插入到右边
如果新结点插入到 parent 的右子树
cpp
parent->_bf++;
因为右子树高度增加了
9 更新平衡因子的三种情况
这是 AVL 插入中最重要的部分之一
更新父亲结点的平衡因子以后,主要有三种情况
text
0
1 或 -1
2 或 -2
9.1 更新后变成0
例如
text
-1 → 0
或者
text
1 → 0
说明原来两边高度不一样
新结点插入到了原来较矮的一边
插入以后两边重新变得一样高
关键点在于
当前子树的高度没有增加
因此不会继续影响父亲结点
所以可以直接结束更新
cpp
if (parent->_bf == 0)
{
break;
}
9.2 更新后变成1或-1
例如
text
0 → 1
或者
text
0 → -1
说明原来左右子树高度相同
插入以后其中一边变高了
当前结点仍然满足 AVL 的平衡要求
但是
当前子树的高度增加了1
因此还可能影响父亲结点
所以需要继续向上更新
cpp
cur = parent;
parent = parent->_parent;
9.3 更新后变成2或-2
例如
text
1 → 2
或者
text
-1 → -2
此时说明当前结点已经失去平衡
必须通过旋转恢复平衡
cpp
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 不平衡了,旋转处理
break;
}
旋转的两个目标
text
1 恢复平衡
2 降低当前子树的高度
当旋转完成后,当前子树的高度恢复到插入之前的状态,因此不会继续影响上一层
所以插入操作可以结束
10 AVL插入代码
核心代码结构如下
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;
}
这里需要特别理解一个问题
为什么更新平衡因子时要同时维护
cpp
cur
parent
因为当前需要判断
cpp
cur
到底是
text
parent的左孩子
还是
text
parent的右孩子
然后决定
cpp
parent->_bf--;
还是
cpp
parent->_bf++;
更新完成以后再把当前结点整体向上移动
cpp
cur = parent;
parent = parent->_parent;
这样就可以继续处理上一层
11 AVL树的旋转
当某个结点的平衡因子变成
text
2
或者
text
-2
说明树已经不平衡
此时需要旋转
AVL树一共有四种旋转情况
text
右单旋
左单旋
左右双旋
右左双旋
旋转必须满足两个原则
原则一
旋转以后仍然必须满足二叉搜索树的大小关系
原则二
旋转以后恢复平衡,并尽可能将树的高度降低到插入之前的高度
12 右单旋
右单旋主要解决
text
左边过高
并且新增结点位于左子树的左侧
典型结构
text
parent
/
subL
/
...
例如
text
10
/
5
/
3
此时
text
10的平衡因子 = -2
需要进行右旋
旋转之后
text
5
/ \
3 10
原来的 5 成为新的根
原来的 10 成为 5 的右孩子
13 右单旋的核心
假设结构为
text
parent
/
subL
/ \
a b
其中
text
a < subL < b < parent
右旋之后
text
subL
/ \
a parent
/
b
为什么 b 可以成为 parent 的左子树
因为满足
text
subL < b < parent
所以不会破坏二叉搜索树的性质
14 右单旋代码
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;
}
右旋最容易出错的地方并不是旋转方向
而是指针关系的修改
需要同时处理
text
孩子指针
父亲指针
_root
上一层结点的孩子指针
15 右旋过程中为什么需要处理subLR
假设
text
parent
/
subL
\
subLR
右旋后
text
subL
\
parent
/
subLR
因此原来的
cpp
subL->_right
必须变成
cpp
parent
而原来的
cpp
subLR
必须移动到
cpp
parent->_left
所以代码中有
cpp
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
这一步非常关键
16 左单旋
左单旋与右单旋完全对称
它主要解决
text
右边过高
例如
text
10
\
15
\
20
此时
text
10的平衡因子 = 2
需要左旋
旋转以后
text
15
/ \
10 20
17 左单旋代码
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;
}
左旋和右旋实际上就是镜像关系
右旋
text
左孩子上升
原根下降到右边
左旋
text
右孩子上升
原根下降到左边
18 左右双旋
有些情况下单旋无法解决问题
例如
text
10
/
5
\
8
此时
text
10左边高
但是新增结点并不是位于 5 的左边,而是位于 5 的右边
因此直接对 10 进行右旋无法彻底解决问题
这就是左右双旋
处理过程
text
先以5为旋转点进行左旋
↓
再以10为旋转点进行右旋
也就是
cpp
RotateL(parent->_left);
RotateR(parent);
19 左右双旋的结构变化
初始
text
10
/
5
\
8
第一次左旋
text
10
/
8
/
5
第二次右旋
text
8
/ \
5 10
这样就重新恢复平衡
20 左右双旋为什么需要特殊处理平衡因子
左右双旋与单旋不同
旋转之前,中间结点的平衡因子可能不同
因此旋转以后三个关键结点的平衡因子并不一定全部为 0
代码需要提前保存中间结点的平衡因子
cpp
int bf = subLR->_bf;
然后进行两次旋转
cpp
RotateL(parent->_left);
RotateR(parent);
最后根据旋转之前保存的 bf 设置三个结点的平衡因子
21 左右双旋代码
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);
}
}
这里最重要的是理解
cpp
int bf = subLR->_bf;
必须在旋转之前保存
因为旋转之后结点之间的关系已经发生变化
如果之后再判断原来的平衡因子,就无法得到原始信息
22 右左双旋
右左双旋与左右双旋完全对称
典型结构
text
10
\
15
/
12
此时
text
10的右边过高
但是新增结点位于 15 的左边
所以不能直接左旋
需要
text
先对15进行右旋
再对10进行左旋
也就是
cpp
RotateR(parent->_right);
RotateL(parent);
23 右左双旋代码
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);
}
}
24 四种旋转如何判断
判断 AVL 树旋转类型时,可以根据失衡结点的平衡因子 以及较高子树根结点的平衡因子来判断
可以记成下面的关系
| 情况 | 结构 | 旋转 |
|---|---|---|
| LL | 左左 | 右单旋 |
| RR | 右右 | 左单旋 |
| LR | 左右 | 左右双旋 |
| RL | 右左 | 右左双旋 |
其中
text
LL
表示失衡结点的左子树的左边更高
text
RR
表示失衡结点的右子树的右边更高
text
LR
表示失衡结点的左子树的右边更高
text
RL
表示失衡结点的右子树的左边更高
最重要的不是死记旋转名称,而是观察
text
失衡发生在哪一侧
新增结点又位于这一侧的哪一边
25 AVL树查找
AVL树本质上还是二叉搜索树
所以查找逻辑与普通二叉搜索树基本一致
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;
}
查找过程
text
key > 当前结点
↓
向右走
key < 当前结点
↓
向左走
key == 当前结点
↓
找到
AVL树通过控制树的高度,使查找效率保持在
text
O(logN)
26 AVL树的平衡检测
实现 AVL 树之后,不能只依赖插入代码判断自己是否正确
还可以编写一个检测函数
通过重新计算每个结点左右子树的高度
然后检查
text
实际平衡因子
和
text
结点保存的_bf
是否一致
27 计算树的高度
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;
}
递归计算高度的基本思想
text
空树高度 = 0
非空树高度 =
max(左子树高度, 右子树高度) + 1
28 判断是否为AVL树
首先计算当前结点左右子树高度
cpp
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
int diff = rightHeight - leftHeight;
这里的 diff 就是根据真实高度重新计算出来的平衡因子
然后检查
cpp
abs(diff) <= 1
同时检查
cpp
root->_bf == diff
也就是说
不仅要检查树是否平衡,还要检查代码维护的 _bf 是否正确
29 AVL树平衡检测代码
cpp
bool _IsBalanceTree(Node* root)
{
if (root == nullptr)
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);
}
这个检测函数实际上检查了两件事情
text
第一
左右子树高度差是否超过1
第二
代码维护的_bf是否等于真实计算出来的平衡因子
只要其中一个条件不满足,就说明 AVL 树实现存在问题
30 AVL树的测试
测试 AVL 树时,可以准备一些容易触发双旋的特殊数据
例如
cpp
int a[] = {
4, 2, 6, 1, 3,
5, 15, 7, 16, 14
};
插入完成后进行中序遍历
cpp
t.InOrder();
再检查
cpp
t.IsBalanceTree();
这样可以同时验证
text
插入逻辑
搜索树性质
旋转逻辑
平衡因子
31 大规模数据测试
除了特殊数据,还可以进行大量随机数据测试
例如
cpp
const int N = 100000;
生成大量数据之后插入 AVL 树
然后统计
text
插入耗时
查找耗时
树高
结点数量
例如
cpp
cout << "Insert:" << end2 - begin2 << endl;
cout << t.IsBalanceTree() << endl;
cout << "Height:" << t.Height() << endl;
cout << "Size:" << t.Size() << endl;
这种测试可以验证 AVL 树在大量数据下是否仍然保持较好的树高和运行效率
32 AVL树的核心代码逻辑
整个 AVL 插入过程可以浓缩成下面这条逻辑链
text
按照BST规则插入
↓
新结点连接parent
↓
从parent开始向上更新_bf
↓
_bf == 0
↓
停止更新
_bf == 1 或 -1
↓
继续向上更新
_bf == 2 或 -2
↓
判断旋转类型
↓
LL → 右单旋
RR → 左单旋
LR → 左右双旋
RL → 右左双旋
↓
恢复平衡
↓
插入结束
真正实现 AVL 树时,最需要关注的并不是某一行代码,而是这几个核心关系
text
二叉搜索树规则
+
parent指针
+
平衡因子
+
四种旋转
+
旋转后的父子指针维护
其中旋转代码最容易出问题的地方主要是
text
1 修改孩子指针
2 修改孩子的parent
3 修改原parent的parent
4 修改上一层结点指向
5 必要时修改_root
6 更新旋转后结点的_bf
AVL树的删除在该实现中没有展开讲解