目录
1.AVL树的概念
2.AVL树的实现
2.1AVL树的结构
2.2 AVL树的插入
2.2.1 AVL树插入的大致过程
2.2.2 平衡因子的更新
2.2.3 插入节点及更新平衡因子的代码实现:
2.3 旋转
2.3.1 旋转的原则
2.3.2 右单旋
2.3.3 左单旋
2.3.4 左右双旋
2.3.5 右左双旋
1.AVL树的概念
- AVL树是自平衡二叉搜索树,可以为一棵空树,或者具备下列性质的二叉搜索树:该树的左右子树也为AVL树,并且左右子树的高度差的绝对值不超过1。AVL树是一颗高度平衡二叉搜索树,通过控制高度差去控制整棵树的平衡。
- AVL树中我们将引入平衡因子(balance factor bf )的概念,树中的每个节点都有一个平衡因子,任何节点的平衡因子就可以反应该节点的左右子树的高度差。在此处我们计算平衡因子(bf)使用右子树高度减去左子树高度,也就是说在该节点的左子树插入时(左子树高度加1),该节点的bf就减1,在该节点的右子树插入时(右子树高度加1),该节点的bf就加1。
- AVL树任何节点都具有平衡因子,因为规定任何节点的左右子树高度差不超过1,因此任何节点的bf就只有-1/0/1三种情况。当我们更新平衡因子的时候,如果不符合该三种情况,说明我们的AVL树出现了问题,此时我们就需要去解决问题,例如旋转操作。
- AVL树节点整体节点数量和分布与完全二叉树类似,高度可以控制在logN,那么AVL树的增删查改的效率也可以控制在O(logN),相比于二叉搜索树的极端情况还是有一些效率的提升。
例如上面的二叉树,乍一眼看上去,好像是很平衡的,但其实其中 10 这个节点已经出现了平衡因子为2的情况,对于10这颗子树来说,已经不是AVL树,因此整棵树也就不是AVL树,我们要做的是通过旋转操作来降低树的高度来使之平衡。
2.AVL树的实现
2.1AVL树的结构
和我们之前实现二叉搜索树一样,我们首先需要一个节点的struct:
template<class K,class V>
struct AVLTreeNode
{
pair<K, V> _value;
AVLTreeNode<K, V>* _parent;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
int _bf;
AVLTreeNode(const pair<K, V>& value)
:_value(value)
, _parent(nullptr)
, _left(nullptr)
, _right(nullptr)
,_bf(0)
{}
};
这里的成员变量想重要说_parent,因为我们需要更新每个节点的平衡因子,我们就需要考虑,该节点的祖先的平衡因子,会不会因为在该节点下面插入新节点,也受到影响。因此我们使用三叉链,可以帮助我们快速的找到一个节点的父节点。
接下来是AVL树的整体大框架:
template<class K, class V>
class AVLTree
{
public:
using Node = AVLTreeNode<K,V>;
private:
//插入代码
//旋转代码
//......
private:
Node* _root = nullptr;
};
2.2 AVL树的插入
2.2.1 AVL树插入的大致过程
1. 插入一个值的时候,按照二叉搜索树的规则进行插入。
2. 新增加节点后,只会影响祖先节点的高度,也就是说可能会影响部分祖先节点的bf,所以更新平衡因子从 新增节点->根节点 路径上的祖先的平衡因子。所以最坏情况就是我们更新到了根节点,最好情况就是插入之后,该节点的高度不变,也就是说平衡因子变为了0,我们便不再向上更新。
3. 更新bf的过程中,如果没有出现-1/0/1外的问题,那么就表明插入之后,该树仍为AVL树,因此直接插入结束。
4. 更新平衡因子过程中如果出现了不平衡,也就是说bf的绝对值大于1,我们此时就需要对不平衡子树进行旋转操作,旋转后本质降低了子树的高度,不会再影响上一层,所以插入结束。
2.2.2 平衡因子的更新
更新规则:
- 平衡因子 = 右子树高度-左子树高度。
- 只有子树高度变化时,才会影响当前节点的平衡因子。
- 插入节点时,会增加高度所以新增节点在parent的右子树,parent的平衡因子++,反之若在左子树,parent的平衡因子-- 。
- parent所在子树的高度是否变化决定了是否会继续往上更新。
更新停止条件:
- 更新后parent的bf = 0,变化过程为 -1->0 或者 1->0,说明了更新前parent子树一边高一边低,新增的节点插入在了低的那边,插入后parent所在子树的高度不变,不会影响parent的父节点的bf,更新结束。
- 更新后parent的bf = 1/-1,变化过程为 0->1 或者 0->-1,说明更新前的parent子树两边一样高,新增节点插入后,导致了parent所在的子树一边高一边低,parent的子树bf符合平衡要求,但是高度增加了1,会影响parent的父节点的bf,因此我们需要继续向上更新。
- 更新后parent的bf = 2/-2,变化过程为 -1->-2 或者 1->2,说明更新前parent的子树一边高一边低,新增节点插入在了高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要进行旋转处理。旋转的目的有两个:1、将parent子树旋转平衡。2、降低parent子树的高度,恢复到插入节点以前的高度。所以旋转之后也不需要继续往上更新,插入结束。
还是上面那张图,当我们新插入13节点的时候,一直往上更新到10节点,发现10节点的平衡因子已经变为了2,此时我们需要停止更新,将10节点进行旋转处理。
2.2.3 插入节点及更新平衡因子的代码实现:
bool _Insert(const pair<K, V>& value)
{
if (_root == nullptr)
{
_root = new Node(value);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (value.first < cur->_value.first)
{
parent = cur;
cur = cur->_left;
}
else if (value.first > cur->_value.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
cur = new Node(value);
if (cur->_value.first < parent->_value.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//该行上方代码,本质上为二叉搜索树插入的逻辑,比较插入的值和该节点的值
//插入的值小向左树走,反之向右树走,相等直接返回即可。
//更新平衡因子
while (parent)
{
//插入的节点在parent的左边,parent的bf--
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,停止更新。
break;
}
else//bf出现异常,快速找到原因
{
assert(false);
}
}
return true;
}
总的来说,插入(目前不包括旋转代码)的大逻辑还是比较简单的,我们按照二叉搜索树的逻辑进行插入,然后更新平衡因子,如果parent的平衡因子为0,则停止更新;为-1/1,继续向上更新,因为我们是三叉链,很容易向上查找;为2/-2,说明parent的左右子树高度不平衡,需要旋转操作,旋转后停止更新。
2.3 旋转
2.3.1 旋转的原则
- 保持搜索树的规则。
- 让旋转的子树从不平衡变平衡,其次降低子树的高度。
旋转总共分为四种:右单选/左单旋/右左双选/左右双旋。
2.3.2 右单旋
这是右单旋的通用图(-2,-1),我们从中可以看到,a,b,c 三棵树的高度均为h,但是实际上,根节点的左子树多了一个节点5,所以对于10来说是左边高右边低,相差高度为1,因此我们在c插入不会影响整棵树的平衡(前提:a,b,c均符合AVL树),同时也不会在c中发生不平衡现象,否则不会旋转10这个节点。所以我们只讨论在a,b中插入。在b插入我们在后面进行讨论,因为牵扯到双旋,此处只讨论右单旋。
首先对于5节点来说,原来的平衡因子为0,因此原来的5左右子树高度均相等,当在a中插入了一个新节点,也就是5的左子树,那么5节点的平衡因子就需要--,变为了-1,我们之前说过,如果是变为了-1/1,就需要继续向上更新,那么直到根节点,变为-2,此时我们就需要进行旋转。
整个结果来说,就是10的左子树高度变高了,因此我们将10定义为parent,5定义为subL,5的右边定义为subLR,然后改变链接关系即可。
先将10的左(subL)变为5的右(subLR),再将5的右变为10,注意:1. 我们这里是三叉链,因此,我们需要将,parent,subL,subLR的父节点(_parent)改变连接方向。如果parent是根节点,我们只需要将subL变为根节点即可,如果parent是一个节点的子树,那么我们需要将subL和parent之前的_parent进行链接。2. 如果a,b,c均为空树,也就是下面的情况:
此时b就是我们定义的subLR为nullptr,对于parent 和 subL,肯定是不为空的,因此我们在链接父子关系时,需要判断一下subLR是否为空。
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
subL->_right = parent;
Node* pParent = parent->_parent;//若,parent父节点存在,则提前记录
//判断subLR是否为空
if (subLR)
subLR->_parent = parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (pParent->_left == parent)
{
pParent->_left = subL;
}
else
{
pParent->_right = subL;
}
subL->_parent = pParent;
}
//最终parent和subL的平衡因子均为0
parent->_bf = 0;
subL->_bf = 0;
}
以上就是右单旋的逻辑。
2.3.3 左单旋
左单旋的通用图(2,1)如下:
此处的逻辑和右单旋类似,只不过我们定义parent,subR,subRL,将subRL变为parent的左子树,再将parent变为subR的左子树,注意链接关系,以及parent是否为根节点即可,并且要判断subRL是否为空(因为a,b,c可能为空树),最终将subR和parent的平衡因子变为0。
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subR->_left;
subR->_left = parent;
Node* pParent = parent->_parent;//提前记录
//判断subRL是否为空
if (subRL)
subRL->_parent = parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (pParent->_left == parent)
pParent->_left = subR;
else
pParent->_right = subR;
subR->_parent = pParent;
}
//将平衡因子变为0
parent->_bf = 0;
subR->_bf = 0;
}
2.3.4 左右双旋
我们先来看看下面的两幅图:
这就是我们之前说的,在b插入,最终的结果都是导致10这个节点导致不平衡,如果我们仅仅进行一个右单旋,我们可以看到,旋转之后的树仍然不是平衡的,上面写的平衡因子为0,是因为我们右单旋的代码,本来就使parent和subL的平衡因子变为了0,因此这个时候我们就需要进行双旋。
我们来看左右双旋的通用图(-2,1):
我们在b插入,最终都导致了10的平衡因子变为了-2,并且我们需要按上图进行旋转,先对5进行左单旋,再对10进行右单旋,我们只看最终的结果图也就是:
左边是新节点放在了subLR的左边后双旋的结果,右边则放在了subLR的右边双旋后的结果。因此我们提前记录subLR的平衡因子,若为-1,说明最终双旋的结果是左边的结果,若为1,说明最终双旋的结果是右边的结果,并且我们可以发现不同结果所对应的平衡因子只有subL和parent不同。
如果a,b,c均为空树:
那么双旋后,三者的平衡因子均为0即可,左右双旋代码如下:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;//提前记录平衡因子,决定最终结果
RotateL(subL);//先左旋
RotateR(parent);//再右旋
if (bf == 0)//a,b,c均为空树情况
{
subLR ->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)//新节点在subLR的右边情况
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else if (bf == -1)//新节点在subLR的左边情况
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else//如果bf出现意外,快速帮我们找到原因
{
assert(false);
}
}
2.3.5 右左双旋
右左双旋和左右双旋的逻辑类似,这里我们直接来看通用图(2,-1):
这里直接看代码,因为逻辑类似:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;//提前记录subRL的bf
RotateR(subR);//先右旋
RotateL(parent);//再左旋
if (bf == 0)//a,b,c均为空树
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)//新节点在subRL的右边
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)//新节点在subRL的左边
{
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else//若出现bf异常,则快速帮我们找到问题
{
assert(false);
}
}
最后我们来补充插入剩下的旋转部分代码:
//插入中部分代码
//...
else if (parent->_bf == -2 || parent->_bf == 2)
{
//旋转
if (parent->_bf == -2 && cur->_bf == -1)//右单旋情况
{
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == 1)//左单旋情况
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)//左右双旋情况
{
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)//右左双旋情况
{
RotateRL(parent);
}
break;
}
//...
以上就是AVL树的学习笔记,供大家参考,如果有任何出错的地方,欢迎大家批评指正!!!