🤡博客主页:醉竺
🥰本文专栏:《高阶数据结构》
😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!
✨✨💜💛想要学习更多《高阶数据结构》点击专栏链接查看💛💜✨✨
目录
1. AVL树删除节点的步骤
2. 删除过程示例(图解演示)
3. AVL树节点删除(图解+代码)
4. AVL树的性能
上一篇文章的我们学习了平衡二叉树的4种插入方式,以及相对应的旋转调整,详情请点击链接:《平衡二叉树(AVL)的插入(4种旋转方法+精美图解+完整代码)》
之所以把AVL树的插入和删除分两篇文章写,主要减少读者的阅读压力。但是由于《二叉树搜索树(BST)》、《平衡二叉树AVL》以及后续要学习的 红黑树(RBT) 之间是递增性的,所以如果是零基础的同学,建议可以先看我写的前几篇文章,每一篇文章我都详细通俗易懂地讲解了不同树地性质和相关操作。下面正式开始学习AVL树节点地删除:
1. AVL树删除节点的步骤
平衡二叉树删除某个节点的操作与二叉查找树删除某个节点的操作非常类似,但删除操作同样会使平衡二叉树失去平衡性。一旦因为删除某个节点导致平衡二叉树失衡,那么也要通过旋转使其恢复为平衡二叉树。删除操作的平衡性调整的实现代码有一定的复杂性,请你认真学习。我们先从删除过程的操作步骤开始说起。
删除过程的三个步骤:
步骤一 , 在平衡二叉树中查找要删除的节点。
步骤二 , 针对所要删除的节点的子节点个数不同, 分别处理3类情况。
注:步骤二的3种情况跟二叉搜索树节点的删除一模一样,下面只列出3种情形,具体地讲解可以看:
步骤三:平衡性调整,这需要从被删除的节点向根节点回溯,也就是从下向上寻找。
-
如果回溯发现所有节点都是平衡的,则不需要调整,因为这表示删除该节点并不影响二叉树的平衡性。
-
如果回溯找到了第一个不平衡的节点(以该节点为根的这棵需要进行平衡性调整的子树,叫做“最小不平衡子树”,这在AVL树节点地插入一文中已详细讲解),这个不平衡节点的平衡因子为-2或2,需要分开讨论。
上述步骤暂时不理解也没关系,继续往后学习,下面有删除节点的例子。
这里提出一个问题:
AVL树节点的插入和删除在调整平衡因子过程中有什么不同?
平衡二叉树的插入只需要从插入节点之父向上检查,发现不平衡立即调整,一次调平衡即可。而删除操作则需要一直从删除节点之父向上检查,发现不平衡立即调整,然后继续向上检查,检查到树根为止。
2. 删除过程示例(图解演示)
AVL树节点删除的具体步骤可以按照下面来,在演示代码之前我会举几个删除节点的例子,看完保证你会掌握 AVL节点的删除。接下来阅读的时候一定要比对着下面的具体步骤,这样可以更清晰地理解删除过程。
例1
例2
例3
例4
例5
例6
2.选择RL型
3. AVL树节点删除(图解+代码)
由上述删除具体步骤 “③找最小不平衡子树下,“个头”(高度)最高的儿子、孙子",和 “④根据孙子的位置,调整平衡(LL/RR/LR/RL)”,我们引出一个问题:在代码中如何找到个头最高的儿子和孙子呢?
答:我们找到最小不平衡子树根节点的时候,假如我们称该最小不平衡子树的根节点为 爷爷节点,则该节点的平衡因子一定是 -2 或者 2。
- 当爷爷节点的平衡因子为-2(小于0)时,说明其左孩子的高度小于右孩子的高度,即“个头”高的儿子是其右孩子(儿子)。也说明删除的节点在爷爷节点的左侧。
- 反之,爷爷的平衡因子为 2(大于0)时,说明爷爷左孩子的高度大于右孩子的高度,即“个头”高的儿子是其左孩子(儿子)。 也说明删除的节点在爷爷节点的右侧。
- 同理,当儿子节点的平衡因子小于0时,说明儿子节点的左孩子高度小于右孩子的高度,即“个头”高的孙子是其右孩子(孙子);
- 当儿子节点的平衡因子大于0时,说明儿子节点的左孩子高度大于右孩子的高度,即“个头”高的孙子是其左孩子(孙子);
上述 1 , 2 两种情形可以与 3, 4 相互组合,孙子节点相对于爷爷节点的位置:就有了 LL型,LR型、RR型和RL型。
平衡因子的更新和调整所有情况如下:
第一种,平衡因子(节点10)为-2,参考图1。这里你可能会碰到三类情况。
-
如果其右孩子(图1左侧图节点12)的平衡因子为-1,则说明该右孩子的右子树(以节点13为根)更高,这种情形等同于RR型插入操作所要进行的平衡性调整,也就是要通过左旋转来恢复二叉树的平衡。
-
如果其右孩子(图1中间图节点12)的平衡因子为1,则说明该右孩子的左子树(以节点11为根)更高,这种情形等同于RL型插入操作所要进行的平衡性调整,也就是要通过先右后左旋转来恢复二叉树的平衡。
-
如果其右孩子(图1右侧图节点12)的平衡因子为0,则既可以通过“左旋转”又可以通过“先右后左旋转”来恢复二叉树的平衡。
对图1所示的三棵二叉树进行平衡性调整后的示意图如图2所示。
第二种情况,如果平衡因子(节点10)为2,参考图3。同样,我们还是会碰到三种情况。
-
如果其左孩子(图3左侧图节点8)的平衡因子为-1,则说明该左孩子的右子树(以节点9为根)更高,这种情形等同于LR型插入操作所要进行的平衡性调整,也就是要通过先左后右旋转来恢复二叉树的平衡。
-
如果其左孩子(图3中间图节点8)的平衡因子为1,则说明该左孩子的左子树(以节点6为根)更高,这种情形等同于LL型插入操作所要进行的平衡性调整,也就是要通过右旋转来恢复二叉树的平衡。
-
如果其左孩子(图3右侧图节点8)的平衡因子为0,则即可以通过“右旋转”又可以通过“先左后右旋转”来恢复二叉树的平衡。
最后,对图3所示的三棵二叉树进行平衡性调整后的示意图如图4所示。
在代码实现中,找到实际需要被删除的结点后,我们先不进行实际的删除,而是先进行平衡因子的更新,不然后续更新平衡因子时特别麻烦(已经尝试过),而更新平衡因子时的规则与插入结点时的规则是相反的,更新规则如下:
- 删除的结点在parent的左边,parent的平衡因子- -。
- 删除的结点在parent的右边,parent的平衡因子+ +。
并且每更新完一个结点的平衡因子后,都需要进行以下判断:
- 如果parent的平衡因子等于-1或者1,表明无需继续往上更新平衡因子了。
- 如果parent的平衡因子等于0,表明还需要继续往上更新平衡因子。
- 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。
判断理由说明:
parent更新后的平衡因子分析
-1或1
只有0经过++ / - -操作后会变成-1/1,说明原来parent的左子树和右子树高度相同,现在我们删除一个结点,并不会影响以parent为根结点的子树的高度,从而变化影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。0
只有-1/1经过++ / - -操作后会变成0,说明本次删除操作使得parent的左右子树当中较高的一棵子树的高度降低了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。-2或2
此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。
代码实现:
//删除函数
bool Erase(const K& key)
{
//用于遍历二叉树
Node* parent = nullptr;
Node* cur = _root;
//用于标记实际的删除结点及其父结点
Node* delParentPos = nullptr;
Node* delPos = nullptr;
while (cur)
{
if (key < cur->_kv.first) //所给key值小于当前结点的key值
{
//往该结点的左子树走
parent = cur;
cur = cur->_left;
}
else if (key > cur->_kv.first) //所给key值大于当前结点的key值
{
//往该结点的右子树走
parent = cur;
cur = cur->_right;
}
else //找到了待删除结点
{
if (cur->_left == nullptr) //待删除结点的左子树为空
{
if (cur == _root) //待删除结点是根结点
{
_root = _root->_right; //让根结点的右子树作为新的根结点
if (_root)
_root->_parent = nullptr;
delete cur; //删除原根结点
return true; //根结点无祖先结点,无需进行平衡因子的更新操作
}
else
{
delParentPos = parent; //标记实际删除结点的父结点
delPos = cur; //标记实际删除的结点
}
break; //待删除结点有祖先结点,需更新平衡因子
}
else if (cur->_right == nullptr) //待删除结点的右子树为空
{
if (cur == _root) //待删除结点是根结点
{
_root = _root->_left; //让根结点的左子树作为新的根结点
if (_root)
_root->_parent = nullptr;
delete cur; //删除原根结点
return true; //根结点无祖先结点,无需进行平衡因子的更新操作
}
else
{
delParentPos = parent; //标记实际删除结点的父结点
delPos = cur; //标记实际删除的结点
}
break; //待删除结点有祖先结点,需更新平衡因子
}
else //待删除结点的左右子树均不为空
{
//替换法删除
//寻找待删除结点右子树当中key值最小的结点作为实际删除结点
Node* minParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minParent = minRight;
minRight = minRight->_left;
}
cur->_kv.first = minRight->_kv.first; //将待删除结点的key改为minRight的key
cur->_kv.second = minRight->_kv.second; //将待删除结点的value改为minRight的value
delParentPos = minParent; //标记实际删除结点的父结点
delPos = minRight; //标记实际删除的结点
break; //删除结点有祖先结点,需更新平衡因子
}
}//end-找到了待删除结点
}//end-while
if (delParentPos == nullptr) //delParentPos没有被修改过,说明没有找到待删除结点
{
return false;
}
//记录待删除结点及其父结点(用于后续实际删除)
Node* del = delPos;
Node* delP = delParentPos;
//更新平衡因子
while (delPos != _root) //最坏一路更新到根结点
{
if (delPos == delParentPos->_left) //delParentPos的左子树高度降低
{
delParentPos->_bf--; //delParentPos的平衡因子--
}
else if (delPos == delParentPos->_right) //delParentPos的右子树高度降低
{
delParentPos->_bf++; //delParentPos的平衡因子++
}
//判断是否更新结束或需要进行旋转
if (delParentPos->_bf == 0)//需要继续往上更新平衡因子
{
//delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
delPos = delParentPos;
delParentPos = delParentPos->_parent;
}
else if (delParentPos->_bf == -1 || delParentPos->_bf == 1) //更新结束
{
break; //delParent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
}
else if (delParentPos->_bf == -2 || delParentPos->_bf == 2) //需要进行旋转(此时delParentPos树已经不平衡了)
{
if (delParentPos->_bf == -2)
{
if (delParentPos->_right->_bf == -1)
{
Node* tmp = delParentPos->_right; //记录delParentPos左旋转后新的根结点
RotateL(delParentPos); //左单旋
delParentPos = tmp; //更新根结点
}
else if (delParentPos->_right->_bf == 1)
{
Node* tmp = delParentPos->_right->_left; //记录delParentPos右左旋转后新的根结点
RotateRL(delParentPos); //右左双旋
delParentPos = tmp; //更新根结点
}
else //delParentPos->_right->_bf == 0
{
Node* tmp = delParentPos->_right; //记录delParentPos左旋转后新的根结点
RotateL(delParentPos); //左单旋
delParentPos = tmp; //更新根结点
//平衡因子调整
delParentPos->_bf = 1;
delParentPos->_left->_bf = -1;
break; //更正
}
}
else //delParentPos->_bf == 2
{
if (delParentPos->_left->_bf == -1)
{
Node* tmp = delParentPos->_left->_right; //记录delParentPos左右旋转后新的根结点
RotateLR(delParentPos); //左右双旋
delParentPos = tmp; //更新根结点
}
else if (delParentPos->_left->_bf == 1)
{
Node* tmp = delParentPos->_left; //记录delParentPos右旋转后新的根结点
RotateR(delParentPos); //右单旋
delParentPos = tmp; //更新根结点
}
else //delParentPos->_left->_bf == 0
{
Node* tmp = delParentPos->_left; //记录delParentPos右旋转后新的根结点
RotateR(delParentPos); //右单旋
delParentPos = tmp; //更新根结点
//平衡因子调整
delParentPos->_bf = -1;
delParentPos->_right->_bf = 1;
break; //更正
}
}
//delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
delPos = delParentPos;
delParentPos = delParentPos->_parent;
//break; //error
}
else
{
assert(false); //在删除前树的平衡因子就有问题
}
}
//进行实际删除
if (del->_left == nullptr) //实际删除结点的左子树为空
{
if (del == delP->_left) //实际删除结点是其父结点的左孩子
{
delP->_left = del->_right;
if (del->_right)
del->_right->_parent = delP;
}
else //实际删除结点是其父结点的右孩子
{
delP->_right = del->_right;
if (del->_right)
del->_right->_parent = delP;
}
}
else //实际删除结点的右子树为空
{
if (del == delP->_left) //实际删除结点是其父结点的左孩子
{
delP->_left = del->_left;
if (del->_left)
del->_left->_parent = delP;
}
else //实际删除结点是其父结点的右孩子
{
delP->_right = del->_left;
if (del->_left)
del->_left->_parent = delP;
}
}
delete del; //实际删除结点
return true;
}
四种插入方式旋转方法调整:LL型插入(RotateR),RR型插入(RotateL),LR(RotateLR),RL(RotateRL) ,相关代码请看平衡二叉树的插入.
4. AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个结点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但当一个结构经常需要被修改时,AVL树就不太适合了。