文章目录
1 概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
关于平衡因子:
平衡因子是左右高度之差,实现时可以定义为右子树高度减左子树高度,也可以定义为左子树高度减右子树高度(因为是用绝对值判断是否合法)平衡因子不是AVL树中必须设计的,只是一种帮助我们控制树的方式,但是使用平衡因子会比较方便。
2 实现
2.1 AVL树结点的定义
AVL树究其本质,也是一棵二叉树,所以结点定义需要包括左右孩子指针,同时还需要一个指向父亲的指针方便之后的操作。另外,AVL树通常是一个<K, V>模型的数据结构,所以内部的数据域用一个pair存储。
template <class K,class V>
struct AVLTreeNode
{
AVLTreeNode* _left;
AVLTreeNode* _right;
AVLTreeNode* _parent;
int _bf; // banance factor(平衡因子),这里用右子树高度减左子树高度表示
pair<K, V> _kv;
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
, _kv(kv)
{}
};
2.2 AVL树的插入
2.2.1 AVL树的插入规则
由于AVL树也是一棵搜索二叉树,因此也需要遵循搜索二叉树的规则进行插入。
如果插入节点大于当前遍历结点,则在当前结点的右子树中继续寻找插入
如果插入节点小于当前遍历结点,则在当前结点的左子树中继续寻找插入
如果插入节点等于当前结点,不能插入,返回false
上面我们也提到,AVL树是高度平衡的二叉搜索树,所以在遵循二叉搜索树插入规则的基础上还需要进行特殊的判断和处理。
显然,在对一棵二叉树进行插入操作时,是有可能改变这棵树的高度的。而对于AVL树,如果执行插入操作后,使得某一个结点的平衡因子绝对值不再为0或者1,那么表示AVL树被破坏了,此时我们就需要执行一些处理维护AVL树。
先来思考一下,如果我们对AVL树进行了插入操作,哪些结点的平衡因子会发生变化呢?
其实很容易发现,当我插入一个结点以后,可能会导致高度发生变化。由于平衡二叉树的特性,就有可能导致该结点到根结点路径上的结点平衡因子都可能会发生变化,那么就需要分情况讨论。
如果插入结点之后,以该结点父亲为根结点的子树高度没有变化,那么对AVL树是没有破坏性的,但是一般来说,父结点的平衡因子会发生变化,如下图1情形,插入结点以后没有影响到父结点为根的子树高度,但是影响父结点的平衡因子需要更新:
这里设成功插入结点后该结点为c,其父结点为p。由我们给定的平衡因子的定义(右子树高度减去左子树高度),如果c为p的左孩子,那么父亲的平衡因子就会减少1;反之就会增加1。
那我们就发现,在AVL树中插入一个结点,一定会影响到的是该结点的父结点的平衡因子,因为插入会导致该父结点的左右子树高度关系发生变化,但是是否会继续向上影响祖先结点呢?这就需要根据情况讨论了。
如下图2,插入结点以后,不仅影响了p的平衡因子,也影响了p父亲的平衡因子(因为p所在子树的高度发生变化。而对于第一张图中的情形,是不会影响到p的父亲的。
我们会发现,上述两张图的区别是,当插入后p的平衡因子变为0时(图1),表示左右子树高度是一样的,那么肯定在插入前左右子树高度相差1,也就是插入前p的平衡因子为±1,此时没有改变p所在子树的整体高度,不会影响p的父亲。
当插入后p的平衡因子变为1时(图2),表示p之前的平衡因子一定是0,插入一个节点后p所在子树的高度一定变化,因此对于p的父亲,其左右子树高度差肯定也会变化。
为什么只能从0变成1,而不能从2变成1?
因为这是在高度平衡的搜索二叉树。如果插入前某个结点的平衡因子为2,表示插入前的树本身就不是一个AVL树。对于合法的AVL树,情况一定如上。
当插入后p的平衡因子变为2(即右子树高度比左子树高2)时,表示这时出现了不合法的AVL树,因此我们就需要对其进行一些处理使其变为AVL树。处理的方式其实就是旋转,下文中将继续叙述。
以上我们总结以下AVL树插入的规则:
- 按照搜索二叉树插入规则插入
- 更新平衡因子
-
更新原则:
如果c是p的左孩子, p -> _bf –
如果c是p的右孩子, p -> _bf ++ -
是否需要继续更新取决于p所在子树的高度是否变化:
如果更新后,p -> _bf == 0,表示更新前p -> _bf 一定为±1,更新之后p的高度没有变化,不会影响p的父亲,结束,返回true如果更新后,p -> _bf == ±1,表示更新前p -> _bf 一定为0,更新之后p变得不均衡,但是并没有违反规则。p的高度一定发生变化,会影响p的父亲,那么需要继续向上更新**(最坏更新到root位置)**
如果更新后,p -> _bf == ±2,表示p所在的子树违反了平衡规则。那么就需要进行特殊处理使其合法,这个处理就是旋转。旋转以后的树就合法了,结束更新(旋转可以让p所在子树高度回到插入之前的状态,不会对上层的平衡因子有影响)
-
综上,更新结束的标志有三点:
- 更新到root位置,结束
- 当前更新位置的parent的平衡因子为0,结束
- 执行旋转操作以后,结束
2.2.2 旋转
旋转的目的:
- 保持搜索规则
- 使当前树从不平衡到平衡
- 降低当前树的高度
以下是旋转的一个示例:
2.2.2.1 左单旋
左单旋的意思就是:一个结点是其父结点的右孩子,经过旋转操作以后,父亲变为该结点的左孩子,而该结点的左孩子变成父亲的右孩子
左单旋发生的场景:
parent -> _bf == 2 && cur -> _bf == 1
void RotateL(Node* parent)
{
Node* subR = parent->_right; // 父亲的右孩子
Node* subRL = subR->_left; // 父亲右孩子的左孩子
parent->_right = subRL;
if(subRl)
subRL->_parent = parent;
subR->_left = parent;
Node* ppnode = parent->_parent;//记录当前父亲的父亲
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == ppnode->_left)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
// 更改平衡因子
subR->_bf = 0;
parent->_bf = 0;
}
旋转的细节:
- 更改结点指针指向时,还要更改对应_parent指针的指向。
- subR的左指针可能为空,注意空指针解引用的问题。
- subR要链接到parent -> _parent 的相应左 / 右指针(与parent的位置一致)
- 如果parent是root,还需要更改root
- 最后要记得修改平衡因子(单旋以后,parent和cur的平衡因子都是0,因此单旋是非常健康的旋转操作)
2.2.2.2 右单旋
右单旋就是左单旋的对称情况,对照左单旋的代码更改即可。
// 右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left; // 父亲的左孩子
Node* subLR = subL->_right; // 父亲左孩子的右孩子
parent->_left = subLR;
// subL可能为空,要注意空指针的问题
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
Node* ppnode = parent->_parent;//记录当前父亲的父亲
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent == ppnode->_left)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
// 更改平衡因子
subL->_bf = 0;
parent->_bf = 0;
}
2.2.2.3 左右双旋
双旋又是什么样的情况呢?我们先以左右双旋的情景来分析。假设有如下情景:
对于如上情形,在30的右边插入结点以后,30的平衡因子变为1,而其父亲的平衡因子变为-2,此时就会触发左右双旋。
如果我们继续按照右单旋的思路执行旋转,读者可以自行尝试,总之最后会发现旋转了个寂寞。
那么就说明,对于上述这种情况,单旋是走不通的,所以就需要新的旋转策略。为了更好的解释这个旋转策略,以以下的例子进行说明
上图中,我们将30的右子树划分为两部分。不难发现,无论新插入的结点在60的左子树还是右子树,都会引发90平衡因子变为-2。
首先先看30为根的这个树,先假设新插入结点在60的左子树,我们来看看对其进行左单旋会发生什么呢
再看这种情况,是不是有点类似于右单旋的样子?此时如果再对90进行右单旋操作
最后得到的树就是合法的AVL树了。
其实,无论第一步的插入是在60的左子树还是右子树,运用上述的策略得到的都是合法的AVL树,如果插入在右子树,旋转之后的树就是这样的。
那么我们就分析出了左右双旋操作的一部分:
前提:cur -> _bf == 1 && parent -> _bf == -2
操作:先对cur执行左单旋;再对parent执行右单旋
分析结束其实可以发现,左右双旋的旋转部分是比较容易得,但是旋转以后还需要修改平衡因子,而双旋平衡因子的修改就比较复杂了。
其实,对于情况1和2,就是上述我们给到的两种旋转示例。从上面两张图中就可以看出,更新之后60的bf永远是0,而30和90的bf就要根据更新之前60的bf来判断。
而如果更新之前,60本身就是要新增的结点,那么在双旋之后,三个结点的bf都应该是0。
所以,更新后的平衡因子如何改变,本质是取决去60这个节点,也就是parent -> _left -> _right的平衡因子是多少。
// 左右双旋
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 = -1;
parent->_bf = 0;
}
else if (bf == -1)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 0)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
![[Pasted image 20240405215056.png]]
2.2.2.4 右左双旋
类比左右双旋,可以写出右左双旋的情况,与左右双旋属于对称情形。
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
2.2.3 总结
根据上述分析,我们就可以写出一个完善的插入函数
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 = cur->_parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 旋转处理
if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
else
{
RotateRL(parent);
}
// 旋转完成以后树就合法了
break;
}
else
{
// 插入之前AVL树已经异常
assert(false);
}
}
return true;
}
3 平衡判断
判断一棵AVL树是否确实平衡其实是有必要的。这里不能单纯按照平衡因子判断,因为我们不能保证插入过程中平衡因子的改变是正确的,而且AVL树是不方便调试的,因此我们需要用最为朴素的方法,即判断树中每一个结点,其两棵子树的高度差是否等于该结点的平衡因子。
这里我们利用递归的方式实现,同时,我们可以利用后序遍历的思想,即从最低层开始判断。如果按照前序遍历的顺序,我们会发现树底部的部分会进行多次重复判断。当我们以后序遍历的顺序判断时,可以通过输出型参数的方式将底部子树高度带出,这样就减少了重复计算。
bool _IsBalence(Node* root,int& height)
{
if (root == nullptr)
{
height = 0;
return true;
}
int leftHeight = 0;
int rightHeight = 0;
// 将左右子树高度以输出型参数的方式带出
if (!_IsBalence(root->_left, leftHeight) || !_IsBalence(root->_right, rightHeight))
{
return false;
}
if (abs(rightHeight - leftHeight) >= 2)
{
cout << "不平衡" << endl;
return false;
}
if (rightHeight - leftHeight != root->_bf)
{
cout << "平衡因子异常" << endl;
return false;
}
// 左右子树的最大高度值加上根节点这一层即为以该结点为根的子树高度
height = max(leftHeight, rightHeight) + 1;
return true;
}
bool IsBalence()
{
int height = 0;
return _IsBalence(_root, height);
}
4 删除
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
但是AVL树的删除相较于插入更为复杂,实际中很少会考察与代码相关的知识,如有兴趣可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。
5 源码
源码中还包含了Height、Size等基础操作。
#include <iostream>
#include <cassert>
using namespace std;
template <class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; // banance factor(平衡因子)
pair<K, V> _kv;
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
, _kv(kv)
{}
};
template <class K, class V>
class AVLTree
{
public:
typedef AVLTreeNode<K,V> Node;
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 = cur->_parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 旋转处理
if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
else
{
RotateRL(parent);
}
// 旋转完成以后树就合法了
break;
}
else
{
// 插入之前AVL树已经异常
assert(false);
}
}
return true;
}
// 左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right; // 父亲的右孩子
Node* subRL = subR->_left; // 父亲右孩子的左孩子
parent->_right = subRL;
// subR可能为空,要注意空指针的问题
if(subRL)
subRL->_parent = parent;
subR->_left = parent;
Node* ppnode = parent->_parent;//记录当前父亲的父亲
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == ppnode->_left)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
// 更改平衡因子
subR->_bf = 0;
parent->_bf = 0;
}
// 右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left; // 父亲的左孩子
Node* subLR = subL->_right; // 父亲左孩子的右孩子
parent->_left = subLR;
// subL可能为空,要注意空指针的问题
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
Node* ppnode = parent->_parent;//记录当前父亲的父亲
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent == ppnode->_left)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
// 更改平衡因子
subL->_bf = 0;
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)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_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)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << "[" << root->_bf << "]" << endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
}
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return max(leftHeight, rightHeight) + 1;
}
int Height()
{
return _Height(_root);
}
bool _IsBalence(Node* root,int& height)
{
if (root == nullptr)
{
height = 0;
return true;
}
int leftHeight = 0;
int rightHeight = 0;
// 将左右子树高度以输出型参数的方式带出
if (!_IsBalence(root->_left, leftHeight) || !_IsBalence(root->_right, rightHeight))
{
return false;
}
if (abs(rightHeight - leftHeight) >= 2)
{
cout << "不平衡" << endl;
return false;
}
if (rightHeight - leftHeight != root->_bf)
{
cout << "平衡因子异常" << endl;
return false;
}
// 左右子树的最大高度值加上根节点这一层即为以该结点为根的子树高度
height = max(leftHeight, rightHeight) + 1;
return true;
}
bool IsBalence()
{
int height = 0;
return _IsBalence(_root, height);
}
size_t _size(Node* root)
{
if (root == nullptr)
return 0;
return _size(root->_left) + _size(root->_right) + 1;
}
size_t 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 NULL;
}
private:
Node* _root = nullptr;
};