AVL树的解析和模拟实现
- 一,什么是AVL树
- 二,AVL树的特性
- 三,模拟实现
- 1. 基本框架
- 2. 插入(不带旋转)
- 2. AVL树的旋转
- 3. AVL树的验证
- 四,总结
一,什么是AVL树
之前我们学习了二叉搜索树,但是有时候因为节点插入的问题,可能会退化为单支树,这样会导致查找效率变得底下如顺序表。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度
这种树就是AVL树.
二,AVL树的特性
AVL树满足两个条件:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
平衡因子=右子树高度-左子树高度
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O
(
l
o
g
2
n
)
O(log_2 n)
O(log2n),搜索时间复杂度O(
l
o
g
2
n
log_2 n
log2n)
三,模拟实现
1. 基本框架
AVL树是一种平衡二叉树,其内部存储的是pair键值对,我们模拟实现的时候直接用pair来存储即可。
我们先来写AVL的节点定义:
每个节点都要有一个平衡因子用来保证其AVL树特性
template<class K,class V>
struct AVLTreeNode {
typedef AVLTreeNode<K, V> Node;
AVLTreeNode(const pair<K,V> &kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
,_kv(kv)
{}
Node* _left;
Node* _right;
Node* _parent;//记录当前节点的父亲
int _bf;//记录节点的平衡因子
pair<K, V> _kv;//保存记录的key,val
};
然后我们来写AVL的框架:
template<class K,class V>
class AVLTree {
typedef AVLTreeNode<K, V> Node;
public:
//...
private:
Node* _root = nullptr;
};
2. 插入(不带旋转)
下面我们来实现AVL树的插入:
插入分为两步:
- 按照二叉搜索树那样插入节点
- 调整平衡因子
插入节点的部分和二叉搜索树的代码一样,主要是修改平衡因子的部分
当插入新节点后,这个新节点的双亲节点的平衡因子会发生变化:
1.新插入的节点cur在其双亲节点parent的左孩子时,parent的平衡因子-1
2. 新插入的节点在parent的右孩子时,parent的平衡因子+1
代码如下:
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 (kv.first > cur->_kv.first) {
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first) {
parent = cur;
cur = cur->_left;
}
else {
return false;
}
}
//找到了插入位置
cur = new Node(kv);
if (kv.first > parent->_kv.first) {
parent->_right = cur;
}
else {
parent->_left = cur;
}
cur->_parent = parent;
//修改平衡因子
while (parent) {
if (cur == parent->_left) {//如果加在左边,则父节点的平衡因子--
parent->_bf--;
}
else {
parent->_bf++;//右边则++
}
//调整平衡因子
//...
}
return true;
}
修改后这里有三种情况:
- 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
- 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更新成正负1,此时以pParent为根的树的高度增加,需要继续向上更新
- 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进行旋转处理
我们先来说前两种情况,第三种我们在旋转中讲解
第一种情况:
修改后parent的平衡因子为0,这里就不用再进行调整了
第二种情况:
修改后平衡因子为1,则以parent为根的这颗树的高度发生了变化,则要继续向上调整平衡因子
代码:
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 (kv.first > cur->_kv.first) {
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first) {
parent = cur;
cur = cur->_left;
}
else {
return false;
}
}
//找到了插入位置
cur = new Node(kv);
if (kv.first > parent->_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) {
//旋转
}
else {
assert(false);//说明插入之前就有问题
}
}
return true;
}
2. AVL树的旋转
上面的两种情况,更新parent的平衡因子后AVL树的特性还保持着,但是第三种情况更新后,双亲的平衡因子为2/-2,破坏了平衡二叉搜索树的特性,所以就要进行以parent为根的树的旋转
AVL树的旋转也分为四种情况
1. 新节点插入较高左子树的左侧:右单旋
2. 新节点插入较高右子树的右侧:左单旋
3. 新节点插入较高左子树的右侧—左右双旋:先左单旋再右单旋
4. 新节点插入较高右子树的左侧—右左双旋:先右单旋再左单旋
下面我们来依次解释:
右单旋:
右单旋是新插入的节点在左子树中,使其整棵树右高左低,所以要旋转右子树来降低高度差使这棵树变得平衡
具体过程看下图:
这里我们来看一下具体当各子树高度的情况:
下面我们来编写一下右单璇的代码:
首先我们定义两个节点用来标记当前需要旋转的节点。
对于右单旋,我们找当前parent的左孩子 subL 和该左孩子的右孩子 subLR
然后我们在旋转时还要注意一下:
(1) 30这个节点的右子树可能存在也可能不存在
不存在时我们就不能直接将subL的parent指针直接指向60
void RotateR(Node* parent) {//右单旋
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) {//当subLR存在时,subLR的parent才指向parent
subLR->_parent = parent;
}
//...
}
(2) 60这个节点可能是根也可能不为根。
不为根时,我们还需要一个 ppnode 节点来标记parent的双亲节点,用来将新的’根’节点去链接到原来的树上。
void RotateR(Node* parent) {//右单旋
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) {//当subLR存在时,subLR的parent才指向parent
subLR->_parent = parent;
}
if (parent == _root) {//如果p是根,则subL更新为根
_root = subL;
subL->_parent = nullptr;
}
else {//将旋转后的子树的根节点链接到原来的树上
if (ppnode->_left == parent) {
ppnode->_left = subL;
}
else {
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
//...
}
最后一步就是修改平衡因子,旋转后的节点不用再调整,因为旋转后子树的两端高度都相等,达到平衡。
void RotateR(Node* parent) {//右单旋
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) {//当subLR存在时,subLR的parent才指向parent
subLR->_parent = parent;
}
subL->_right = parent;
Node* ppnode = parent->_parent;//保存p的parent指向
parent->_parent = subL;
if (parent == _root) {//如果p是根,则subL更新为根
_root = subL;
subL->_parent = nullptr;
}
else {//将旋转后的子树的根节点链接到原来的树上
if (ppnode->_left == parent) {
ppnode->_left = subL;
}
else {
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
//更新节点的平衡因子
parent->_bf = 0;
subL->_bf = 0;
}
现在来看左单旋:
左单旋和右单旋相反,左边高右边低,所以进行左单旋来降低高度差
这里的情况都和右单旋转相反,我们直接上代码:
void RotateL(Node* parent) {//左单旋
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) {//当subRL存在时,subRL的parent才指向parent
subRL->_parent = parent;
}
subR->_left = parent;
Node* ppnode = parent->_parent;//保存p的parent指向
parent->_parent = subR;
if (parent == _root) {//如果p是根,则subR更新为根
_root = subR;
subR->_parent = nullptr;
}
else {//将旋转后的子树的根节点链接到原来的树上
if (ppnode->_left == parent) {
ppnode->_left = subR;
}
else {
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
//更新节点的平衡因子
parent->_bf = 0;
subR->_bf = 0;
}
我们现在来看左右双旋,先左单旋再右单旋
这里的左右双旋其实就是相当右单旋的那种场景下,将b子树拆分成两颗子树,再将新增节点加在拆分后的子树上
我们以加在左子树为例讲解:
对新增节点后的树进行旋转,先以subL为根进行左旋,
再对parent为根进行右旋
加在右子树的过程和这个相反,希望大家可以自己去推导…
知道了大概的过程,我们现在来写代码:
和单旋一样,我们定义变量来协助我们旋转
void RotateRL(Node* parent) {//双旋(先左单旋,再右单旋)
Node* subL = parent->_left;
Node* subLR = subL->_right;
//..
}
这里需要注意:新增节点所加的位置有三种情况
(1) 加在拆分后的左子树上
(2) 加在拆分后的右子树上
(3) 上图的60这个位置本身是空的
那么我们如何区分这三种情况呢,
我们可以用subLR的平衡因子来区分,subLR的因子为-1时,说明左高,则加在了左子树上,为1时,说明右高,则加在了右子树,为0时说明这个位置原来是空的。
void RotateLR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
subLR->_bf = 0;
if (bf == -1) {
parent->_bf = 1;
subL->_bf = 0;
}else if (bf == 1) {
parent->_bf = 0;
subL->_bf = -1;
}
else if (bf == 0) {
parent->_bf = 0;
subL->_bf = 0;
}
else {
assert(false);
}
subLR->_bf = 0;
}
右左双旋
右左双旋和左右双旋相反,各位老铁可以参考左右双旋来自己去画出图来理解
代码:
void RotateRL(Node* parent) {//双旋(先右单旋,再左单旋)
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;//以subRL的因子为标准判断所加的子树的位置
RotateR(subR);
RotateL(parent);
//修改平衡因子
//增加节点后有三种情况
subRL->_bf = 0;
if (bf == -1) {//加在左子树上
parent->_bf = 0;
subR->_bf = 1;
}
else if (bf == 1) {
parent->_bf = -1;
subR->_bf = 0;
}
else if (bf == 0) {
parent->_bf = 0;
subR->_bf = 0;
}
else {
assert(false);
}
}
3. AVL树的验证
AVL的插入讲完了,我们来看看如何证明咋们模拟实现的就是AVL树
一棵树如果是AVL树,那么首先它是一个二叉搜索树,其次他的每个子树的高度差不能超过1
这里我们做了一点小优化,我们在判断时先传入高度,判断高度差时类似于后序遍历的方式,从下往上去计算高度差
代码如下:
bool IsBalance() {
int height = 0;
return _IsBalance(_root, height);
}
bool _IsBalance(Node* root,int &height) {
if (root == nullptr) {
height = 0;
return true;
}
int leftHeight = 0, rightHeight = 0;
if (!_IsBalance(root->_left, leftHeight) || !_IsBalance(root->_right, rightHeight)) {
return false;
}
if (abs(rightHeight - leftHeight) >= 2) {
cout <<root->_kv.first<< "不平衡" << endl;
return false;
}
if (rightHeight - leftHeight != root->_bf) {
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
return true;
}
四,总结
我们今天终于讲完了AVL树,我们的C++部分也开始上了难度,希望大家可以跟上我们的讲解,下一节我们要来开始手撕红黑树
!!!
在此之前我们要熟悉前面的二叉搜索树和AVL树的内容,希望大家都能学好C++。