由于二叉搜索树在某些特定的情况下会退化成单叉树,为了解决这个问题,保证二叉搜索树能在绝大多数情况下保持高速搜索,G.M. Adelson-Velsky和E.M. Landis这两位俄国数学家提出了AVL树的概念,也就是高度平衡的搜索二叉树。
AVL树平衡大体逻辑:其具体的实现逻辑则是借助检查平衡因子来实现搜索二叉树的平衡,当平衡因子失衡的时候则旋转。
平衡因子:衡量当前节点的左右子树高度差的变量,当一个新节点插入右树则+1,插入左树则-1。所以左右子树高度之差(即平衡因子)的绝对值不超过1(-1/0/1),当超过1或-1时即达到失衡。
AVL树的实现
节点结构的定义:
template<class K,class V>
struct AVLNode
{
pair<K, V> _kv; kv结构
AVLNode<K,V>* _left; 左节点
AVLNode<K,V>* _right; 右节点
AVLNode<K, V>* _parent; 父节点
int _bf; 平衡因子
AVLNode(const pair<K,V> &kv)
:_kv(kv),_left(nullptr), _right(nullptr),_parent(nullptr), _bf(0)
{}
};
AVL树节点的插入
AVL树的插入节点则比搜索二叉树更加复杂,其整体的实现逻辑顺序如下:
1.以搜索二叉树的插入逻辑完成最基本的节点插入,即比当前节点大就放入右子树,比当前节点小就放入左子树
2.检查并更新平衡因子,插入的节点是右边则当前节点的父节点的平衡因子+1,反之则-1,假如父节点的平衡因子是0则不需要再向上检查,若是1或者-1则需要向上更新平衡因子,最坏的情况则是更新到根。
3.失衡时旋转,当父节点的平衡因子等于2或者-2时,此时的AVL树已经失衡,需要旋转,具体的旋转分为多种情况,下文再细致讨论。
旋转情况
具体的旋转情况比较复杂,所以使用抽象图来概括整体的情况。旋转有4种情况。
- 情况1:新节点插入较高左子树的左侧,右单旋
既然两个节点高度都加1时会触发左旋,那么我们以a子树的高度+1为例,画出旋转过程。旋转的本质是降低树的高度,在不破坏搜索二叉树的属性的情况下交换子树。
旋转过程中还需要注意一些细节:
- 如果是根节点,旋转完成后,要更新根节点
- 如果是子树,可能是某个节点的左子树,也可能是右子树
- 40节点的右孩子可能存在,也可能不存在
- 50可能是根节点,也可能是子树
那么右单旋的代码如下:
void RotateR(Node* parent)
{
Node* pparent = parent->_parent;
Node* cur = parent->_left;
parent->_left = cur->_right;
//如果cur的左子树不等于空,才链接过去
if (cur->_right)
cur->_right->_parent = parent;
parent->_parent = cur;
cur->_right = parent;
if (pparent == nullptr)
{
_root = cur;
_root->_parent = nullptr;
}
else
{
//pparent的孩子发生变动,判断是左子树还是右子树
//若原先变动前的parent是pprant的左树
if (pparent->_left == parent)
pparent->_left = cur;
else
pparent->_right = cur;
cur->_parent = pparent;
}
- 情况2:新节点插入较高右子树的右侧:左单旋
此时以50为轴点进行旋转
同情况一的代码逻辑相同,只不过指针的指向被更换了。
void RotateL(Node* parent)
{
//旋转
//parent变量所指向的节点一定是旋转轴点
// parent->right = parent->_right->_left
// 那么parent的右孩子的_left指向parent, 也就是
// parent->_right->left = parent
// 但还是需要额外处理一个问题,假如这次旋转只是处理了一个子树时,parent的right还需要更换祖宗
// 所以还要一个pparent
Node* pparent = parent->_parent;
Node* cur = parent->_right;
parent->_right = cur->_left;
//如果cur的左子树不等于空,才链接过去
if (cur->_left)
cur->_left->_parent = parent;
cur->_left = parent;
parent->_parent = cur;
//等于空,旋转了根节点
if (pparent == nullptr)
{
_root = cur;
_root->_parent = nullptr;
}
else//不等于空,旋转了一个子树
{
//pparent的孩子发生变动,判断是左子树还是右子树
//若原先变动前的parent是pprant的左树
if (pparent->_left == parent)
pparent->_left = cur;
else
pparent->_right = cur;
cur->_parent = pparent;
}
//旋转完毕,还需要更新平衡因子
parent->_bf = cur->_bf = 0;
}
- 情况3:新节点插入较高左子树的右侧---左右双旋:先左单旋再右单旋
本情况也就是情况1的另一边,由于此时单次旋转已无法解决问题,需要左右双旋转。
为什么需要左右双旋?
为了更好的解释这个问题,需要将b节点的形状具象化,而非抽象化,b
此时并不需要旋转,假如此时b节点的高度+1,AVL树又会进入失衡状态
此时假如我们再次同情况一进行右旋转,会发现无法解决问题
树的整体形状从一个右折线被旋转成了左折线。
所以此时我们需要使用左右双旋,先将整体从折线变为直线,再进行旋转降低高度。
如何左右双旋?以上图为例,b的高度增加
注:此图中的60节点数值有误,应为30
这样,整个AVL树可以再次进入平衡,那么假如换成d的高度增加,其实也是相同的旋转方法。只不过子树的位置变动了
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
//左右双旋结束后,还需要更新正在这条折线上的平衡因子
//平衡因子的更新情况有三种,因为造成左右双旋的原因是因为新增加的节点造成了折线式的失衡,才需要先左旋再右旋
//那么针对折线底端的那个节点,既然是它造成了折线失衡,那么就需要处理当前节点的三种失衡情况,
//1.当前节点增加在了它的右树,使其平衡因子+1
//2.当前平衡因子增加在了它的左树,使其平衡因子-1
//3.当前新增的节点就是其本身,平衡因子为0
if (bf == -1) // subLR左子树新增
{
subL->_bf = 0;
parent->_bf = 1;
subLR->_bf = 0;
}
else if (bf == 1) // subLR右子树新增
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == 0) // subLR自己就是新增
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
- 情况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) // subLR左子树新增
{
subR->_bf = 1;
parent->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 1) // subLR右子树新增
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 0) // subLR自己就是新增
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
当pSubR的平衡因子为1时,执行左单旋
当pSubR的平衡因子为-1时,执行右左双旋
2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
当pSubL的平衡因子为-1是,执行右单旋
当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
根据以上结论,整理逻辑即可写出来AVL树的插入函数
bool Insert(const pair<K,V> &kv)
{
//如果树为空,则创建一个节点
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
// 如果不为空,则找寻插入的位置,以Key作为插入根据,对比Key的大小
//先检查当前插入的值应该往哪去
Node* cur = _root;
Node* parent = nullptr;
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 (parent->_kv.first < cur->_kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
//新建完节点之后,要更新平衡因子
//插入右树则+1,插入左树则-1,当父亲节点的平衡因子为0的时候停止,最多修正至根
while (parent)
{
//判断平衡因子该加还是该减少
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
//移动完毕,平衡因子会有三种情况。等于0,不动,等于1,说明有变动,向上移动继续调整,最坏情况直到根节点
//父亲的平衡因子等于0,不必再更新,直接break
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)
//此时AVL树已经失衡,需要旋转来修正
{
//旋转分为左旋转和右旋转,当更改来自右树时,也就是parent的bf==2,其右数的bf==1时,左旋转
//右树失衡,左旋转
if (parent->_bf == 2 && cur->_bf == 1)
RotateL(parent);
//左树失衡,右旋转
else if (parent->_bf == -2 && cur->_bf == -1)
RotateR(parent);
//假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者 - 2,分以下情况考虑
// 1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
// 当pSubR的平衡因子为1时,执行左单旋
// 当pSubR的平衡因子为 - 1时,执行右左双旋
// 2. pParent的平衡因子为 - 2,说明pParent的左子树高,设pParent的左子树的根为pSubL
// 当pSubL的平衡因子为 - 1是,执行右单旋
// 当pSubL的平衡因子为1时,执行左右双旋
// 旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
else
assert(false);
break;
}
else//如果出现超过2的平衡因子,说明AVL树已经严重失衡,直接断死,不需要在做处理,整棵树的逻辑肯定出了问题
{
assert(false);
}
}
return true;
}
AVL树的验证
为了验证这颗AVL树是否真的达到了我们想要实现的功能,我们还需要实现一个检测函数来检查其高度,不能检查平衡因子,毕竟平衡因子是我们设定并更新的,假如我们只是简单的检查平衡因子,则有监守自盗的嫌疑,所以参考学习二叉树学习的一个求取二叉树的高度,我们实现两个函数走一个递归检测AVL树。
求取当前节点的左右子树高度
int Height(Node* root)
{
if (root == nullptr)
return 0;
int left = Height(root->_left) + 1;
int right = Height(root->_right) + 1;
return max(left,right);
}
利用高度,同平衡因子比较
bool IsBalance(Node* root)
{
if (root == nullptr)
return true;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
这样,一颗AVL树就成功的实现了基本的功能
删除较为复杂,不记述