【数据结构】AVL树
A V L AVL AVL 树是最先发明的自平衡二叉搜索树。在 A V L AVL AVL 树中任何节点的两个子树的高度最大差别为 1 1 1(也就是 0 ≤ 0 \le 0≤ 平衡因子 ≤ 1 \le 1 ≤1),所以它也被称为高度平衡树。 A V L AVL AVL 树的插入和删除可以通过旋转(单旋或双旋)来自动平衡整棵树,其解决了普通的二叉搜索树极端不平衡而导致的最坏时间复杂度为 O ( N ) O(N) O(N) 的情况。
文章目录
- 一、AVL 树的概念
- 二、AVL 树的基本操作
- 1. AVL 树的结构
- 2. 插入操作
- 2.1 插入结点
- 2.2 平衡因子更新
- 3. 旋转操作
- 3.1 旋转的原则
- 3.2 单旋
- (1) 右单旋
- (2) 左单旋
- 3.3 双旋
- (1) 左右双旋
- (2) 右左双旋
- 4. 查找操作
- 5. 平衡检测
- 三、AVL 树的实现
- 总结
一、AVL 树的概念
A V L AVL AVL 树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。
A V L AVL AVL 树是最先发明的自平衡二叉查找树, A V L AVL AVL 是一颗空树,或者具备下列性质的二叉搜索树:
-
它的左右子树都是 A V L AVL AVL 树。
-
左右子树的高度差的绝对值不超过 1 1 1。
比如下图就是一个标准的 A V L AVL AVL 树:
A V L AVL AVL 树实现这里我们引入一个平衡因子( b a l a n c e f a c t o r balance\ factor balance factor)的概念,每个结点都有一个平衡因子,
任何结点的平衡因子 = 右子树的高度 − 左子树的高度 任何结点的平衡因子=右子树的高度-左子树的高度 任何结点的平衡因子=右子树的高度−左子树的高度
,也就是说在 A V L AVL AVL 树中
任何结点的平衡因子 = 0 / 1 / − 1 任何结点的平衡因子= 0/1/-1 任何结点的平衡因子=0/1/−1
, A V L AVL AVL 树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。
比如下图这颗树就不是一个 A V L AVL AVL 树,因为其 10 10 10 这个结点的平衡因子为 2 2 2:
为什么 A V L AVL AVL 树是高度平衡搜索二叉树,要求高度差不超过 1 1 1,而不是高度差是 0 0 0 呢? 0 0 0 不是更好的平衡吗?
因为有些情况是做不到高度差是 0 0 0 的:比如一棵树是 2 2 2 个结点、 4 4 4 个结点等情况下,高度差最好就是 1 1 1,无法做到高度差是 0 0 0。
A V L AVL AVL 树整体结点数量和分布和完全二叉树类似,高度可以控制在 log N \log N logN,那么增删查改的效率也可以控制在 O ( log N ) O(\log N) O(logN),相比二叉搜索树有了本质的提升。
二、AVL 树的基本操作
1. AVL 树的结构
A V L AVL AVL 树本质上也是一颗二叉搜索树,但 A V L AVL AVL 树结点存的是一个三叉链,不仅要存储左右子树的根结点,还要存储其父结点,以及一个平衡因子。
- A V L AVL AVL 树的结点:
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){}
};
- A V L AVL AVL 树模板类:
template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:// ...
private:Node* _root = nullptr;
};
2. 插入操作
2.1 插入结点
由于 A V L AVL AVL 树本质上是一个二叉搜索树,因此插入操作应该也满足二叉搜索树的插入操作,只不过插入数据会导致树的不平衡,之后会进行旋转操作来解决这个问题。
A V L AVL AVL 树插入一个值的大概过程:
-
插入一个值按二叉搜索树规则进行插入。
-
新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点到根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了,具体情况我们下面再详细分析。
-
更新平衡因子过程中没有出现问题,则插入结束
-
更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。
bool insert(const pair<K, V>& kv)
{if (_root == nullptr){_root = new Node(kv);}Node* prev = nullptr;Node* cur = _root;while (cur){if (kv->first < cur->_kv->first){prev = cur;cur = cur->_left;}else if (kv->first > cur->_kv->first){prev = cur;cur = cur->_right;}else{return false;}}cur = new Node(kv);if (kv->first < prev->_kv->first){prev->_left = cur;}else{prev->_right = cur;}cur->_parent = prev;// 平衡因子的更新// ...return true;
}
2.2 平衡因子更新
A V L AVL AVL 树不一定非要用平衡因子来实现,也可以通过一个容器来存储插入结点的所有祖先,但我们这里选择用平衡因子 + + + p a r e n t parent parent 结点(三叉链)的方式来实现会更加直观一些,更好理解。
【更新原则】
-
平衡因子 = = = 右子树高度 − - − 左子树高度。
-
只有子树高度变化才会影响当前结点平衡因子。
-
插入结点,会增加高度,所以新增结点在 p a r e n t parent parent 的右子树, p a r e n t parent parent 的平衡因子 + + ++ ++,新增结点在 p a r e n t parent parent 的左子树, p a r e n t parent parent 平衡因子 − − -- −−。
-
p a r e n t parent parent 所在子树的高度是否变化决定了是否会继续往上更新。
【更新停止条件】
- 更新后 p a r e n t parent parent 的平衡因子等于 0 0 0,更新结束。
更新中 p a r e n t parent parent 的平衡因子变化为 -1->0
或者 1->0
,说明更新前 p a r e n t parent parent 子树一边高一边低,新增的结点插入在低的那边,插入后 p a r e n t parent parent 所在的子树高度不变,因此就不会影响 p a r e n t parent parent 的父亲结点的平衡因子,更新结束。
如上图,以 3 3 3 结点为根的子树高度不变,因此就不会影响上一层的 8 8 8 结点。
- 更新后 p a r e n t parent parent 的平衡因子等于 1 1 1 或 − 1 -1 −1,继续向上更新。
更新前更新中 p a r e n t parent parent 的平衡因子变化为 0->1
或者 0->-1
,说明更新前 p a r e n t parent parent 子树两边一样高,新增的插入结点后, p a r e n t parent parent 所在的子树一边高一边低, p a r e n t parent parent 所在的子树符合平衡要求,但是高度增加了 1 1 1,会影响 p a r e n t parent parent 的父亲结点的平衡因子,所以要继续向上更新。
如上图,插入 − 1 -1 −1 结点,其 p a r e n t parent parent( 1 1 1 结点)的平衡因子更新为 − 1 -1 −1,因此需要继续向上更新 3 3 3 结点。
- 更新后 p a r e n t parent parent 的平衡因子等于 2 2 2 或 − 2 -2 −2,要进行旋转操作,旋转后也不需要继续往上更新。
更新前更新中 p a r e n t parent parent 的平衡因子变化为 1->2
或者 -1->-2
,说明更新前 p a r e n t parent parent 子树一边高一边低,新增的插入结点在高的那边, p a r e n t parent parent 所在的子树高的那边更高了,破坏了平衡, p a r e n t parent parent 所在的子树不符合平衡要求,需要旋转处理,旋转的目标两个:
( 1 1 1)把 p a r e n t parent parent 子树旋转平衡。
( 2 2 2)降低 p a r e n t parent parent 子树的高度,恢复到插入结点以前的高度。
所以旋转后也不需要继续往上更新,插入结束。
如上图,更新到 10 10 10 结点,平衡因子为 2 2 2, 10 10 10 结点所在的子树已经不平衡,需要旋转处理。
- 不断更新,更新到根,根的平衡因子是 1 1 1 或 − 1 -1 −1 也就停止了。(最坏情况)
如上图,每次更新 p a r e n t parent parent 的平衡因子都被更新为 1 / − 1 1/-1 1/−1,因此需要不断向上更新,最坏更新到根停止。
// 平衡因子的更新
while (parent)
{// 更新平衡因子if (cur == prev->_left)prev->_bf--;elseprev->_bf++;if (prev->_bf == 0){break;}else if (prev->_bf == 1 || prev->_bf == -1){cur = parent;prev = prev->_parent;}else if (prev->_bf == 2 || prev->_bf == -2){// 旋转// ...break;}else{assert(false);}
}
3. 旋转操作
旋转总共分为四种:左单旋 / / / 右单旋 / / / 左右双旋 / / / 右左双旋。
3.1 旋转的原则
无论是哪种旋转,都必须满足以下三种旋转的基本原则。
-
保持搜索树的规则。(比如:左子树所有结点都比根结点小,右子树所有结点都比根结点大)
-
让旋转的树从不平衡变平衡。
-
降低旋转树的高度。
3.2 单旋
如果增加的是 a a a 树的高度,那么就可以直接将 s u b L / s u b R subL/subR subL/subR 作为根结点,以 p a r e n t parent parent 为轴旋转,也就是说,当 p a r e n t parent parent 和 s u b L / s u b R subL/subR subL/subR 平衡因子同号的时候,采用单旋逻辑。
(1) 右单旋
图 1 1 1 展示的是一棵以 10 10 10 为根的树,有三棵高度为 h h h 的子树( h ≥ 0 h\ge0 h≥0)分别抽象为 a / b / c a/b/c a/b/c, a / b / c a/b/c a/b/c 均符合 A V L AVL AVL 树的要求。
在 a a a 子树中插入一个新结点,导致 a a a 子树的高度从 h h h 变成了 h + 1 h+1 h+1,不断向上更新平衡因子,导致 10 10 10 的平衡因子从 − 1 -1 −1 变成了 − 2 -2 −2,以 10 10 10 为根的树左右高度差超过 1 1 1,违反了平衡规则。
因此,以 10 10 10 为根的树左边太高了,需要往右边旋转(以 10 10 10 为旋转点),控制两棵树的平衡。
旋核心步骤:
-
b b b 变成 10 10 10 的左子树;
-
10 10 10 变成 5 5 5 的右子树;
-
5 5 5 变成这棵树新的根。
本质上可以看作把 a a a 多出来的那部分高度 1 1 1,用 10 10 10 这个结点来补充了: a ( h + 1 ) = b ( h ) + 10 ( 1 ) a(h+1)=b(h)+10(1) a(h+1)=b(h)+10(1)
符合旋转的原则:
( 1 1 1) 5 < b < 10 5 < b < 10 5<b<10,旋转前后都符合搜索树的规则;
( 2 2 2)这棵树旋转后由不平衡变为平衡;
( 3 3 3)这棵树的高度由 h + 3 h+3 h+3 恢复到了插入结点之前的 h + 2 h+2 h+2(高度降低)。
void rotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;Node* pParent = parent->_parent;// 旋转核心逻辑subL->_right = parent;parent->_left = subLR;// 处理parentif (subLR)subLR->_parent = parent;parent->_parent = subL;if (_root == parent) // pParent == nullptr{_root = subL;subL->_parent = nullptr;}else{if (pParent->_left == parent){pParent->_left = subL;}else{pParent->_right = subL;}subL->_parent = pParent;}// 处理平衡因子subL->_bf = parent->_bf = 0;
}
上述情况中,把 a / b / c a/b/c a/b/c 概括抽象表示为了高度为 h h h 的子树,代表了所有右单旋的场景,而实际上,右单旋的形态根据插入前 a / b / c a/b/c a/b/c 的高度 h h h 而分为很多种情况:
【第一种情况】插入前 a / b / c a/b/c a/b/c 的高度 h = 0 h=0 h=0:
即 a / b / c a/b/c a/b/c 都为空树。
【第二种情况】插入前 a / b / c a/b/c a/b/c 的高度 h = 1 h=1 h=1:
即 a / b / c a/b/c a/b/c 都代表一个(根)结点,结构唯一。
【第三种情况】插入前 a / b / c a/b/c a/b/c 的高度 h = 2 h=2 h=2:
即 a / b / c a/b/c a/b/c 为高度为 2 2 2 的 A V L AVL AVL 树,此时就有多种情况了:
-
第一层只能是一个结点。
-
第二层分为 3 3 3 种情况:
( 1 1 1)有两个结点 —— —— —— x x x 。
( 2 2 2)只有左结点 —— —— —— y y y 。
( 3 3 3)只有右结点 —— —— —— z z z 。
其中, a a a 只能是 x x x(只有这种情况才能确保插入任意结点都能使高度变为 h + 1 h+1 h+1),而 b / c b/c b/c 可以是 3 3 3 种情况里的任意一种(只要保证高度为 h h h 即可)。
【第四种情况】插入前 a / b / c a/b/c a/b/c 的高度 h = 3 h=3 h=3:
即 a / b / c a/b/c a/b/c 为高度为 3 3 3 的 A V L AVL AVL 树,此时情况就更加复杂了(这里采用排列组合的方式更好推广):
-
第一层只能是一个结点。
-
第二层只能是两个结点。(高度为 h h h 必须保证前 h − 1 h-1 h−1 层是满二叉树,不然会存在旋转问题)
-
第三层分为 ∑ k = 1 4 C 4 k = C 4 1 + C 4 2 + C 4 3 + C 4 4 = 15 \sum\limits_{k=1}^{4}C_{4}^{k}=C_{4}^{1}+C_{4}^{2}+C_{4}^{3}+C_{4}^{4}=15 k=1∑4C4k=C41+C42+C43+C44=15 种情况。
根据上述规律,很容易推广到高度 h = n h=n h=n 的情况,但我们只需要重点关注抽象出来的普适情况即可。
(2) 左单旋
图 6 6 6 展示的是一棵以 10 10 10 为根的树,有三棵高度为 h h h 的子树( h ≥ 0 h\ge0 h≥0)分别抽象为 a / b / c a/b/c a/b/c, a / b / c a/b/c a/b/c 均符合 A V L AVL AVL 树的要求。
在 a a a 子树中插入一个新结点,导致 a a a 子树的高度从 h h h 变成了 h + 1 h+1 h+1,不断向上更新平衡因子,导致 10 10 10 的平衡因子从 1 1 1 变成了 2 2 2,以 10 10 10 为根的树左右高度差超过 1 1 1,违反了平衡规则。
因此,以 10 10 10 为根的树右边太高了,需要往左边旋转(以 10 10 10 为旋转点),控制两棵树的平衡。
旋转核心步骤:
-
b b b 变成 10 10 10 的右子树;
-
10 10 10 变成 15 15 15 的左子树;
-
15 15 15 变成这棵树新的根。
本质上可以看作把 a a a 多出来的那部分高度 1 1 1,用 10 10 10 这个结点来补充了: a ( h + 1 ) = b ( h ) + 10 ( 1 ) a(h+1)=b(h)+10(1) a(h+1)=b(h)+10(1)
符合旋转的原则:
( 1 1 1) 10 < b < 15 10 < b < 15 10<b<15,旋转前后都符合搜索树的规则;
( 2 2 2)这棵树旋转后由不平衡变为平衡;
( 3 3 3)这棵树的高度由 h + 3 h+3 h+3 恢复到了插入结点之前的 h + 2 h+2 h+2(高度降低)。
void rotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;Node* pParent = parent->_parent;// 旋转核心逻辑subR->_left = parent;parent->_right = subRL;// 处理parentif (subRL)subRL->_parent = parent;parent->_parent = subR;if (_root == parent) // pParent == nullptr{_root = subR;subR->_parent = nullptr;}else{if (pParent->_left == parent){pParent->_left = subR;}else{pParent->_right = subR;}subR->_parent = pParent;}// 处理平衡因子subR->_bf = parent->_bf = 0;
}
上述情况中,把 a / b / c a/b/c a/b/c 概括抽象表示为了高度为 h h h 的子树,代表了所有左单旋的场景,而实际上,左单旋的形态根据插入前 a / b / c a/b/c a/b/c 的高度 h h h 而分为很多种情况,具体情况和上面右单旋类似,在此不再展开介绍。
3.3 双旋
如果增加的是 b b b 树的高度,按照单旋的逻辑就无法解决问题了,也就是说,当 p a r e n t parent parent 和 s u b L / s u b R subL/subR subL/subR 平衡因子异号的时候,采用双旋逻辑。
双旋的本质上是先把两边高的情况变为一边高的情况,然后就转化为单旋可以解决的问题了。
(1) 左右双旋
通过图 b b b 我们可以发现,如果插入位置不是在 a a a 子树,而是插入在 b b b 子树, b b b 子树高度从 h h h 变成 h + 1 h+1 h+1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。
这里通过图 7 7 7、图 8 8 8 两种情况来举例:
【第一种情况】插入前 a / b / c a/b/c a/b/c 的高度 h = 0 h=0 h=0(即 a / b / c a/b/c a/b/c 都为空树):
插入结点在 b b b 子树中,以 10 10 10 为根的子树不再是单纯的左边高,对于 10 10 10 是左边高,但是对于 5 5 5 是右边高,因此经过一次右单旋还是不平衡(对于 5 5 5 还是右边高,对于 10 10 10 还是左边高):
【第二种情况】插入前 a / b / c a/b/c a/b/c 的高度 h = 1 h=1 h=1(即 a / b / c a/b/c a/b/c 都为一个结点):
插入结点在 b b b 子树中,以 10 10 10 为根的子树不再是单纯的左边高,对于 10 10 10 是左边高,但是对于 5 5 5 是右边高,因此经过一次右单旋还是不平衡(对于 5 5 5 还是右边高,对于 10 10 10 还是左边高):
由此可见,右单旋只能解决纯粹的左边高问题,如果对于 p a r e n t parent parent 是左边高,而对于 s u b sub sub 是右边高,就需要用两次旋转(左右双旋,先左旋再右旋)才能使树平衡:
-
以 5 5 5 为旋转点进行一个左单旋。
-
以 10 10 10 为旋转点进行一个右单旋。
下面我们将 a / b / c a/b/c a/b/c 子树抽象为高度 h h h 的 A V L AVL AVL 子树进行分析:
图 9 9 9 展示的是一棵以 10 10 10 为根的树,有三棵高度为 h h h 的子树( h ≥ 0 h\ge0 h≥0)分别抽象为 a / b / c a/b/c a/b/c( a / b / c a/b/c a/b/c 均符合 A V L AVL AVL 树的要求)。
旋转的逻辑很简单,复用之前的代码即可:
void rotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;// 先左旋,再右旋rotateL(parent->_left);rotateR(parent);// 更新平衡因子// ...
}
这里主要难点在于如何去更新平衡因子:
另外我们需要把 b b b 子树的细节进一步展开为 8 8 8 和左子树高度为 h − 1 h-1 h−1 的 e e e 和 f f f 子树,因为我们要对 b b b 的父亲结点 5 5 5 为旋转点进行左单旋,左单旋需要动 b b b 树中的左子树。
由于 b b b 子树中新增结点的位置不同,平衡因子更新的细节也不同,因此为了更新平衡因子,这里要分三个场景讨论:
【场景 1 1 1】 h ≥ 1 h\ge1 h≥1 时,新增结点插入在 e e e 子树:
e e e 子树高度从 h − 1 h-1 h−1 变为 h h h 并不断更新 8 → 5 → 10 8\to5\to10 8→5→10 的平衡因子,引发旋转:
状态 | 8 8 8 的平衡因子 | 5 5 5 的平衡因子 | 10 10 10 的平衡因子 |
---|---|---|---|
旋转前 | − 1 -1 −1 | 1 1 1 | − 2 -2 −2 |
旋转后 | 0 0 0 | 0 0 0 | 1 1 1 |
【场景 2 2 2】 h ≥ 1 h\ge1 h≥1 时,新增结点插入在 f f f 子树:
f f f 子树高度从 h − 1 h-1 h−1 变为 h h h 并不断更新 8 → 5 → 10 8\to5\to10 8→5→10 的平衡因子,引发旋转:
状态 | 8 8 8 的平衡因子 | 5 5 5 的平衡因子 | 10 10 10 的平衡因子 |
---|---|---|---|
旋转前 | 1 1 1 | 1 1 1 | − 2 -2 −2 |
旋转后 | 0 0 0 | − 1 -1 −1 | 0 0 0 |
【场景 3 3 3】 h = 0 h=0 h=0 时( a / b / c a/b/c a/b/c 都是空树):
b b b 自己就是一个新增结点,不断更新 5 → 10 5\to10 5→10 的平衡因子,引发旋转:
状态 | 8 8 8 的平衡因子 | 5 5 5 的平衡因子 | 10 10 10 的平衡因子 |
---|---|---|---|
旋转前 | 0 0 0 | 1 1 1 | − 2 -2 −2 |
旋转后 | 0 0 0 | 0 0 0 | 0 0 0 |
// 更新平衡因子
if (bf == -1)
{subLR->_bf = 0;subL->_bf = 0;parent->_bf = 1;
}
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(false);
}
(2) 右左双旋
跟左右双旋类似,下面我们将 a / b / c a/b/c a/b/c 子树抽象为高度 h h h 的 A V L AVL AVL 子树进行分析:
图 13 13 13 展示的是一棵以 10 10 10 为根的树,有三棵高度为 h h h 的子树( h ≥ 0 h\ge0 h≥0)分别抽象为 a / b / c a/b/c a/b/c( a / b / c a/b/c a/b/c 均符合 A V L AVL AVL 树的要求)。
旋转的逻辑很简单,复用之前的代码即可:
void rotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;// 先右旋,再左旋rotateR(parent->_right);rotateL(parent);// 更新平衡因子// ...
}
这里主要难点在于如何去更新平衡因子:
另外我们需要把 b b b 子树的细节进一步展开为 12 12 12 和左子树高度为 h − 1 h-1 h−1 的 e e e 和 f f f 子树,因为我们要对 b b b 的父亲结点 15 15 15 为旋转点进行右单旋,右单旋需要动 b b b 树中的右子树。
由于 b b b 子树中新增结点的位置不同,平衡因子更新的细节也不同,因此为了更新平衡因子,这里要分三个场景讨论:
【场景 1 1 1】 h ≥ 1 h\ge1 h≥1 时,新增结点插入在 e e e 子树:
e e e 子树高度从 h − 1 h-1 h−1 变为 h h h 并不断更新 12 → 15 → 10 12\to15\to10 12→15→10 的平衡因子,引发旋转:
状态 | 12 12 12 的平衡因子 | 15 15 15 的平衡因子 | 10 10 10 的平衡因子 |
---|---|---|---|
旋转前 | − 1 -1 −1 | − 1 -1 −1 | 2 2 2 |
旋转后 | 0 0 0 | 1 1 1 | 0 0 0 |
【场景 2 2 2】 h ≥ 1 h\ge1 h≥1 时,新增结点插入在 f f f 子树:
f f f 子树高度从 h − 1 h-1 h−1 变为 h h h 并不断更新 12 → 15 → 10 12\to15\to10 12→15→10 的平衡因子,引发旋转:
状态 | 12 12 12 的平衡因子 | 15 15 15 的平衡因子 | 10 10 10 的平衡因子 |
---|---|---|---|
旋转前 | 1 1 1 | − 1 -1 −1 | 2 2 2 |
旋转后 | 0 0 0 | 0 0 0 | − 1 -1 −1 |
【场景 3 3 3】 h = 0 h=0 h=0 时( a / b / c a/b/c a/b/c 都是空树):
b b b 自己就是一个新增结点,不断更新 15 → 10 15\to10 15→10 的平衡因子,引发旋转:
状态 | 12 12 12 的平衡因子 | 15 15 15 的平衡因子 | 10 10 10 的平衡因子 |
---|---|---|---|
旋转前 | 0 0 0 | − 1 -1 −1 | 2 2 2 |
旋转后 | 0 0 0 | 0 0 0 | 0 0 0 |
// 更新平衡因子
if (bf == -1)
{subRL->_bf = 0;subR->_bf = 1;parent->_bf = 0;
}
else if (bf == 1)
{subRL->_bf = 0;subR->_bf = 0;parent->_bf = -1;
}
else if (bf == 0)
{subRL->_bf = 0;subR->_bf = 0;parent->_bf = 0;
}
else
{assert(false);
}
4. 查找操作
A V L AVL AVL 树本质上是一棵二叉搜索树,而查找操作和二叉搜索树的操作完全一致,因此直接使用二叉搜索树逻辑实现即可(类似于一棵完全二叉搜索树),搜索效率为 O ( log N ) O(\log N) O(logN)。
Node* find(const K& key)
{Node* cur = _root;while (cur){if (key < cur->_kv.first){cur = cur->_left;}else if (key > cur->_kv.first){cur = cur->_right;}else{return cur;}}return nullptr;
}
5. 平衡检测
实现平衡检测的目的是为了检测我们实现的 A V L AVL AVL 树是否合格,通过检查左右子树的高度差的程序进行反向验证,同时检查一下结点的平衡因子更新是否出现了问题。
首先求高度(即求二叉树的高度):
int _height(Node* root)
{if (root == nullptr)return 0;int lh = _height(root->_left);int rh = _height(root->_right);return lh > rh ? lh + 1 : rh + 1;
}
根据右子树和左子树的高度差( d = r h − l h d=rh-lh d=rh−lh)来判断 A V L AVL AVL 树是否合格:
bool _isBalanceTree(Node* root)
{if (root == nullptr)return true;int lh = _height(root->_left);int rh = _height(root->_right);int d = rh - lh;if (abs(d) >= 2){cout << "高度差异常!" << endl;return false;}else if (root->_bf != d){cout << "平衡因子异常!" << endl;return false;}return _isBalanceTree(root->_left) && _isBalanceTree(root->_right);
}
三、AVL 树的实现
由于这个数据结构是用 C C C++ 代码来模拟实现的,因此采用了模板来定义的 A V L AVL AVL 树类,所以不能将声明和定义分离,因此这里分为了两个文件: A V L T r e e . h AVLTree.h AVLTree.h 来模拟实现并封装一个 A V L AVL AVL 树的模板类, t e s t . c p p test.cpp test.cpp 用来测试。
原理部分已经交代清楚了,这里给出完整代码:
- A V L T r e e . h AVLTree.h AVLTree.h:
#pragma once
#include<iostream>
#include<cassert>
#include<vector>using namespace std;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){}
};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);}Node* prev = nullptr;Node* cur = _root;while (cur){if (kv.first < cur->_kv.first){prev = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first){prev = cur;cur = cur->_right;}else{return false;}}cur = new Node(kv);if (kv.first < prev->_kv.first){prev->_left = cur;}else{prev->_right = cur;}cur->_parent = prev;// 平衡因子的更新while (prev){// 更新平衡因子if (cur == prev->_left)prev->_bf--;elseprev->_bf++;if (prev->_bf == 0){break;}else if (prev->_bf == 1 || prev->_bf == -1){cur = prev;prev = prev->_parent;}else if (prev->_bf == 2 || prev->_bf == -2){// 旋转if (prev->_bf == -2 && cur->_bf == -1){rotateR(prev);}else if (prev->_bf == 2 && cur->_bf == 1){rotateL(prev);}else if (prev->_bf == -2 && cur->_bf == 1){rotateLR(prev);}else if (prev->_bf == 2 && cur->_bf == -1){rotateRL(prev);}break;}else{assert(false);}}return true;}void rotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;Node* pParent = parent->_parent;// 旋转核心逻辑subL->_right = parent;parent->_left = subLR;// 处理parentif (subLR)subLR->_parent = parent;parent->_parent = subL;if (_root == parent) // pParent == nullptr{_root = subL;subL->_parent = nullptr;}else{if (pParent->_left == parent){pParent->_left = subL;}else{pParent->_right = subL;}subL->_parent = pParent;}// 处理平衡因子subL->_bf = parent->_bf = 0;}void rotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;Node* pParent = parent->_parent;// 旋转核心逻辑subR->_left = parent;parent->_right = subRL;// 处理parentif (subRL)subRL->_parent = parent;parent->_parent = subR;if (_root == parent) // pParent == nullptr{_root = subR;subR->_parent = nullptr;}else{if (pParent->_left == parent){pParent->_left = subR;}else{pParent->_right = subR;}subR->_parent = pParent;}// 处理平衡因子subR->_bf = parent->_bf = 0;}void rotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;// 先左旋,再右旋rotateL(parent->_left);rotateR(parent);// 更新平衡因子if (bf == -1){subLR->_bf = 0;subL->_bf = 0;parent->_bf = 1;}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(false);}}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 = 1;parent->_bf = 0;}else if (bf == 1){subRL->_bf = 0;subR->_bf = 0;parent->_bf = -1;}else if (bf == 0){subRL->_bf = 0;subR->_bf = 0;parent->_bf = 0;}else{assert(false);}}Node* find(const K& key){Node* cur = _root;while (cur){if (key < cur->_kv.first){cur = cur->_left;}else if (key > cur->_kv.first){cur = cur->_right;}else{return cur;}}return nullptr;}void inorder(){_inorder(_root);cout << endl;}int height(){return _height(_root);}int size(){return _size(_root);}bool isBalanceTree(){return _isBalanceTree(_root);}private:bool _isBalanceTree(Node* root){if (root == nullptr)return true;int lh = _height(root->_left);int rh = _height(root->_right);int d = rh - lh;if (abs(d) >= 2){cout << "高度差异常!" << endl;return false;}else if (root->_bf != d){cout << "平衡因子异常!" << endl;return false;}return _isBalanceTree(root->_left) && _isBalanceTree(root->_right);}int _height(Node* root){if (root == nullptr)return 0;int lh = _height(root->_left);int rh = _height(root->_right);return lh > rh ? lh + 1 : rh + 1;}int _size(Node* root){if (root == nullptr)return 0;return _size(root->_left) + _size(root->_right) + 1;}void _inorder(Node* root){if (root == nullptr)return;_inorder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << " ";_inorder(root->_right);}Node* _root = nullptr;
};
- t e s t . c p p test.cpp test.cpp:
#include"AVLTree.h"template<class K, class V>
void isAVLTree(AVLTree<K, V>& t)
{if (t.isBalanceTree()){cout << "是AVL树!" << endl;}else{cout << "不是AVL树:" << endl;}
}void test1()
{// 常规测试用例//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };// 特殊的带有双旋的测试用例int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int, int> t;for (auto e : a){t.insert(make_pair(e,e));}t.inorder();cout << t.height() << endl;isAVLTree(t);cout << "height:" << t.height() << " 层" << endl;cout << "size: " << t.size() << " 个" << endl;
}void test2()
{const int N = 1e6;vector<int> v;v.reserve(N);srand(time(0));for (int i = 0; i < N; i++){v.push_back(rand() + i);}AVLTree<int, int> t;// 插入一堆随机值,测试平衡,顺便测试一下高度和性能size_t begin1 = clock();for (auto& e : v){t.insert(make_pair(e, e));}size_t end1 = clock();cout << "insert:" << end1 - begin1 << " ms" << endl;isAVLTree(t);cout << "height:" << t.height() << " 层" << endl;cout << "size: " << t.size() << " 个" << endl;cout << endl;size_t begin2 = clock();// 确定在的值for (auto& e : v){t.find(e);}size_t end2 = clock();size_t begin3 = clock();// 随机值for (size_t i = 0; i < N; i++){t.find((rand() + i));}size_t end3 = clock();cout << "find确定值:" << end2 - begin2 << " ms" << endl;cout << "find随机值:" << end3 - begin3 << " ms" << endl;
}int main()
{//test1();test2();return 0;
}
总结
以上就是对 A V L AVL AVL 树的基本介绍,通过 A V L AVL AVL 树的插入操作,引出了旋转操作,详细地解释了 A V L AVL AVL 树(自平衡二叉搜索树)是如何通过单旋和双旋等操作来实现自平衡的,而对于 A V L AVL AVL 树的删除操作这里不做过多介绍,和之前二叉搜索树的删除类似,不同的是要更新平衡因子,更新平衡因子也可以参考插入操作,二者比较相似。
其实 A V L AVL AVL 树本质上还是一棵二叉搜索树,其各种逻辑还是逃脱不了其底层,只是在其中加入了旋转操作,优化了普通搜索树的结构,使其结构更像一棵树,也就是说, A V L AVL AVL 树的增删改查等各种操作,时间复杂度都是 O ( log N ) O(\log N) O(logN) 的。