👍作者主页:进击的1++
🤩 专栏链接:【1++的数据结构】
一,什么是AVL树
在说AVL树之前我们先来说说为什么会出现AVL树。在前面的文章中我们讲过二叉搜索树,虽然查找,插入效率比较高,但其有个缺陷:在某些情况下其可能会成为一颗单支树或其他高度较高的树,这时我们的效率就比较低了,甚至接近于O(n)。在此背景下,有两位俄罗斯数学家,便发明了AVL树----当插入新的结点后,保证每个结点的左右子树的高度差不大于1。则这颗树就会接近满二叉树,其高度就会降低,那么查找,插入效率也会提高。
二,AVL树的插入
在说AVL树的插入之前我们先来了解其结点。与二叉搜索树不同,其是三叉链结构和平衡因子,不但有左右指针,还有指向双亲结点的指针。这么设计的好处在于后面更新平衡因子是会比较方便。
我们来看结点结构代码:
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
pair<K, V> _kv;
int _bf;//平衡因子
AVLTreeNode(const std::pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{}
};
了解了结点的结构后,我们就可以进行插入操作的学习。
与二叉搜索树一样,其先要找到要插入的位置,代码如下;
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->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
与以前不同的是,由于是三叉链结构,因此当找到位置后,我们还要与其双亲结点进行链接。
当找到合适的位置进行插入后,便要进行控制平衡,使每个结点的左右子树高度差不超过1。
每个根节点的平衡因子的计算过程为;若插入的结点在其左侧,则根节点的平衡因子-1;若在右侧,则平衡因子+1。
根据平衡因子,我们总结出了三种控制平衡的规律:
- 若插入后根节点的平衡因子变为0,则说明在插入前其平衡因子为正负1,插入后被调整为0,满足AVL树的性质,则不做任何操作。
- 若插入后根节点的平衡因子变为正负1,则说明在插入前其平衡因子为0,插入后被改变,则向上继续更新,直到根节点。
- 若插入后平衡因子变为正负2,则违反了AVL树平衡的性质,需要进行旋转处理。
旋转操作,我们会在后面进行说明。
以下是控制其平衡的代码:
while (parent)
{
if (cur == parent->_right)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
if (parent->_bf == 0)
{
break;
}
else if (abs(parent->_bf) == 1)
{
parent = parent->_parent;
cur = cur->_parent;
}
else if (abs(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 if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
break;
}
else
{
assert(false);
}
}
}
三,AVL树的旋转
3.1 向左旋转
以下是向左 旋转的代码:
void RotateL(Node* parent)
{
Node* SubR = parent->_right;
Node* SubRL = SubR ->_left;
parent->_right = SubRL;
if (SubRL)
{
SubRL->_parent = parent;
}
Node* ppNode = parent->_parent;
SubR->_left = parent;
parent->_parent = SubR;
if (_root == parent)
{
_root = SubR;
SubR->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = SubR;
}
else
{
ppNode->_right = SubR;
}
SubR->_parent = ppNode;
}
SubR->_bf = parent->_bf = 0;
}
那么在什么情况下才会进行向左旋转呢?
如图,当我们的新插入的结点在较高子数的右侧时,此时30这个结点的平衡因子变为了2,满足了旋转的条件(图中h可为任何正整数或0)我们可以从图中清楚的看到,当插入新节点后平衡因子为2的结点的右子数明显高,为了平衡,我们则需要给其右子树将高度,我们将30称为parent,60称为SubR;60的左子树b称为SubRL。从图中我们看到,SubR成为了这颗树或者说是子树的新结点。而SubRL成为了parent的右子树。改变后仍遵循二叉搜索树的规则。此时,我们的左旋转就完成了。旋转后,平衡因子也发生了改变,所以要对平衡因子进行更新。对parent和SubR的平衡因子进行更新,更新后都变为0。
3.2 向右旋转
向右旋转与向左旋转相似
代码如下:
void RotateR(Node* parent)
{
Node* SubL = parent->_left;
Node* SubLR = SubL->_right;
parent->_left = SubLR;
if (SubLR)
{
SubLR->_parent = parent;
}
Node* ppNode = parent->_parent;
SubL->_right = parent;
parent->_parent = SubL;
if (_root == parent)
{
_root = SubL;
SubL->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = SubL;
}
else
{
ppNode->_right = SubL;
}
SubL->_parent = ppNode;
}
SubL->_bf = parent->_bf = 0;
}
向右旋转的情况如下图:
当新插入的结点在较高左子树的左侧时,进行右旋转。
如图,我们可以看出其左子树高,我们仍将60称为parent,将30称为SubL,将30的右子树称为SubLR。旋转时,将SubL作为了根节点,SubLR给了parent的左侧。旋转完成后要进行平衡因子的更新,parent与SubL都更新为0。
3.3 左右双旋
代码如下:
void RotateLR(Node* parent)
{
Node* SubL = parent->_left;
Node* SubLR = SubL->_right;
int bf = SubLR->_bf;
RotateL(parent->_left);
RotateR(parent);
SubLR->_bf = 0;
if (bf == 1)
{
parent->_bf = 0;
SubL->_bf = -1;
}
else if (bf == -1)
{
parent->_bf = 1;
SubL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
SubL->_bf = 0;
}
else
{
assert(false);
}
}
当新结点插入较高左子树的右侧时,则会进行左右双旋,就是先向左旋转,再向右旋转。
先以30为parent进行左旋转,再以90为parent进行右旋转。
左右旋转我们在上面已经进行了讲述,接下来,我们的重点是平衡 因此的更新,在上图这种情况下,新插入的结点可以为60的左子树,也可以为右子树,或者60本身就为新插入的结点。
因此在不能的情况下,平衡因子的更新也不同。
主要有以下情况:
(SubL为30,parent为90)
Node* SubL= parent->_left;
Node* SubLR = SubL->_right;
int bf = SubLR->_bf;
SubLR->_bf = 0;
if (bf == 1)
{
parent->_bf = 0;
SubL->_bf = -1;
}
else if (bf == -1)
{
parent->_bf = 1;
SubL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
SubL->_bf = 0;
}
3.4 右左双旋
代码如下:
void RotateRL(Node* parent)
{
Node* SubR = parent->_right;
Node* SubRL = SubR->_left;
int bf = SubRL->_bf;
RotateR(parent->_right);
RotateL(parent);
SubRL->_bf = 0;
if (bf == 1)
{
parent->_bf = -1;
SubR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
SubR->_bf = 1;
}
else if (bf == 0)
{
parent->_bf = 0;
SubR->_bf = 0;
}
else
{
assert(false);
}
}
当新结点插入在较高右子树的左侧时,进行右左双旋(先向右旋转,在向左旋转)。
与左右双旋相似,其平衡因子的更新也有几种情况:
SubRL->_bf = 0;
if (bf == 1)
{
parent->_bf = -1;
SubR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
SubR->_bf = 1;
}
else if (bf == 0)
{
parent->_bf = 0;
SubR->_bf = 0;
}
else
{
assert(false);
}
四,验证AVL树是否平衡
AVL树最重要的一个性质就是其每个结点的左右子树之差的绝对值小于2。
并且等于该结点的平衡因子的绝对值。
我们以此来测试我们所写的AVL树是否正确
代码如下:
//求该子树的高度
int Height(Node* root)
{
if (root == nullptr)
{
return 0;
}
return max(Height(root->_left), Height(root->_right)) + 1;
}
bool _Isbalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int leftHT = Height(root->_left);
int rightHT = Height(root->_right);
int diff = rightHT - leftHT;
if (diff != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return abs(diff) < 2
&& _Isbalance(root->_left)
&& _Isbalance(root->_right);
}