前言
在上一篇博客中,我们提到了AVL树的创建销毁,核心是插入数据,(不了解的可以先看这篇博客AVL树超详解上-优快云博客)这次我们来看AVL树的另一核心功能删除,有了插入,我们一定也会有对数据删除的操作,接下来让我们遨游在AVL的深海中吧。
首先明确一个概念,在二叉树中,祖先结点是指从根节点到给定节点所经过的分支上的所有节点。
例如下图的祖先9的祖先为1 3 6
AVL树的查找
AVL树的查找实现较为简单。将key与当前结点比较,如果大于当前结点就遍历右子树,小于就遍历左子树,等于就返回当前结点指针,查询不到就返回空指针。与二叉搜索树的查找机会一样。
node* find(T k)
{
node* cur = _root;
while (cur)
{
//k大于结点值就去右边,小于就去左边
if (cur->_key < k)
cur = cur->_right;
else if (cur->_key > k)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
上面一次排除一半不可能的数据与二分查找类似,时间复杂度都是O(lgN),是十分快速的。
AVL树的删除
在上一篇文章就提过,AVL树是一种特殊的二叉搜索树,我们AVL树的删除就可以借用二叉搜索树的删除来帮我们简化些思路。
删除数据
在这一小步骤中,我们可以分多种情况来帮我们解决问题.其中我们把删除函数的返回值类型设置为bool,如果在AVL树中找不到删除结点就返回false。
1.AVL树为空树
此时我们是不可能成功删除数据的,所以要返回false。
2.删除结点没有孩子
此时我们的第一步骤就是找到删除结点,就可以利用find函数。快速查到删除结点。
此时我们就可以得到如下代码
bool erase(T k)
{
//空树
if (_root == nullptr)
return false;
node* cur = find(k);
node* parent = cur->_parent;
if (cur == nullptr)
return false;
//没有孩子
if (cur->_left == nullptr && cur->_right == nullptr)
{
//根节点特殊处理
if (parent == nullptr)
{
_root = nullptr;
delete cur;
return true;
}
if (parent->_left == cur)
parent->_left = nullptr;
else
parent->_right = nullptr;
delete cur;
}
return true;
}
但此时处理是不够的,我们还要更新删除结点祖先的平衡因子,即其父亲一族。我们在最后在统一处理,先处理删除结点的情况。
3.删除结点有一个孩子
例如我们要删除3结点,只需要将3的父亲结点连接3唯一的孩子,再删除3即可。
再例如下图删除14结点,只需要将8与13链接,再删除14结点就可以了。
于是我们便可以将这一种情况代码写出
//一个孩子
if (cur->_left == nullptr)
{
//根节点特殊处理
if (parent == nullptr)
{
_root = cur->_right;
delete cur;
return true;
}
if (parent->_left == cur)
{
parent->_left = cur->_right;
cur->_right->_parent = parent;
}
else
{
parent->_right = cur->_right;
cur->_left->_parent = parent;
}
delete cur;
}
else if (cur->_right == nullptr)
{
//根节点特殊处理
if (parent == nullptr)
{
_root = cur->_left;
delete cur;
return true;
}
if (parent->_left == cur)
{
parent->_left = cur->_left;
cur->_left->_parent = parent;
}
else
{
parent->_right = cur->_left;
cur->_left->_parent = parent;
}
delete cur;
}
4.删除结点有两个孩子
如下图删除3结点,
假如要删除3,我们想要最大利用原来的数据结构,不从头再来重新插入。可以分析下3位置的数有什么特点。 将3删除后,替代他的数一定再他的左右子树中,这个数要大于左子树任意节点值,又要小于右子树任意值。
通过观察图像,可以发现只有左子树最右边的值即左子树最大值,和右子树最左边的值即右子树最小值满足要求。
于是我们便可以找到上面两个特殊值其中的一个替代删除结点,这里以左子树最大值为例。
于是我们便可以得到如下的代码
//删除两个结点
//找到左子树最大值
node* subL = cur->_left;
node* subParentMax = cur;
while (subL)
{
subParentMax = subL;
subL = subL->_right;
}
//删除
cur->_key = subParentMax->_key;
if (subParentMax->_left == subL)
subParentMax->_left = nullptr;
else
subParentMax->_right = nullptr;
delete subL;
到此我们就将结点删除了,但我们还要更新平衡因子。接下来就按照我们划分的情况来更新平衡因子。
截止到上述的全部代码。其实我们可以将第二种情况看成第三种的特殊情况,进而简化代码。
bool erase(T k)
{
//空树
if (_root == nullptr)
return false;
node* cur = find(k);
node* parent = cur->_parent;
if (cur == nullptr)
return false;
//没有孩子
if (cur->_left == nullptr && cur->_right == nullptr)
{
//根节点特殊处理
if (parent == nullptr)
{
_root = nullptr;
delete cur;
return true;
}
if (parent->_left == cur)
parent->_left = nullptr;
else
parent->_right = nullptr;
delete cur;
}
//一个孩子
if (cur->_left == nullptr)
{
//根节点特殊处理
if (parent == nullptr)
{
_root = cur->_right;
delete cur;
return true;
}
if (parent->_left == cur)
{
parent->_left = cur->_right;
cur->_right->_parent = parent;
}
else
{
parent->_right = cur->_right;
cur->_left->_parent = parent;
}
delete cur;
}
else if (cur->_right == nullptr)
{
//根节点特殊处理
if (parent == nullptr)
{
_root = cur->_left;
delete cur;
return true;
}
if (parent->_left == cur)
{
parent->_left = cur->_left;
cur->_left->_parent = parent;
}
else
{
parent->_right = cur->_left;
cur->_left->_parent = parent;
}
delete cur;
}
//删除两个结点
//找到左子树最大值
node* subL = cur->_left;
node* subParentMax = cur;
while (subL)
{
subParentMax = subL;
subL = subL->_right;
}
//删除
cur->_key = subParentMax->_key;
if (subParentMax->_left == subL)
subParentMax->_left = nullptr;
else
subParentMax->_right = nullptr;
delete subL;
//更新平衡因子
return true;
}
更新平衡因子
我们通过上述的删除可以发现我们最终实际删除的结点都是叶子结点,而不会删除AVL树中其他结点,假如我们删除AVL树中其他结点,这样会破坏AVL树的结构,不利于我们使用AVL树,所以接下来我们只需要讨论从叶子结点向上更新平衡因子就可以了。
例如下面我们删除结点4,标出他们原来的平衡因子。为了论述方便,这里我们任然规定平衡因子等于右子树的高度减去左子树的高度。
通过上图和之前分析我们可以知道删除一个节点后,平衡因子改变的只会是删除结点的祖先,即一直向上找父亲,对于其他结点的平衡因子无影响。
平衡因子具体如何变化,我们可以逐步分析,
当我们删除的是parent左子树结点时,由于这个是叶子结点,一定会引起6的左子树高度减一,平衡因子为右子树高度减去左子树高度,所以6的平衡因子加一。
此时由于6原来有两个孩子,删去一个不影响以6为结点的AVL树高度就无需在向上更新高度了。
所以最终的结果为下图
上述的情况具有特殊性,我们可以将上述的情况抽象出来,离开具体的事例分析问题。
1.删除结点后的父亲平衡因子为1/-1
这里为了方便我们以删除后父亲结点的平衡因子来不同讨论。
首先我们看的是删除结点后父亲平衡因子为1/-1,我们从第一个父亲结点开始分析,要想第一个parent结点在删除cur节点后的平衡因子为1/-1,那么原来的a结点左右必然都有孩子,只有这种情况在删除节点后,父亲结点a的平衡因子才会变成1/-1.
此时不论我们删除a结点的左孩子还是右孩子结点,以a为根结点的AVL树高度不变,如下图
既然他的高度与原来一样那么我们就没有必要再去更新a结点的祖先了,他们的平衡因子也一定不会变化。
2.删除结点后的父亲平衡因子为0
同样我们从第一个父亲结点开始分析,要想第一个parent结点在删除cur节点后的平衡因子为0,那么原来的a结点左右必然只有一个孩子,只有这种情况在删除孩子结点后,父亲结点a的平衡因子才会变成0.如下图。
此时我们发现二叉树的高度减小一了,我们就要向上不断更新祖先的平衡因子。那该如何修改平衡因子呢?
与插入时修改类似,如果cur是parent的右孩子,parent的平衡因子就减一,如果cur是parent的左孩子,parent的平衡因子就加一,我们可以简单的记为左加右减。
3.删除结点后的父亲平衡因子为2/-2
此时我们就必须对当前结点做出处理了,进行相应的旋转,旋转操作和插入时是一样的, 至于采用哪种旋转方式要根据cur与parent的平衡因子判断,详情可以看上篇博客AVL树超详解上-优快云博客,在这里就不过多的赘述了。
例如下图删除结点5后,结点2的平衡因子为-2,结点4的平衡因子为1,就要进行左右双旋,旋转后的结果如下图。
旋转后的结果。我们可以发现1的左子树在旋转后的高度减小了一,于是我们就需要继续向上进行更新平衡因子,而不是像插入操作时,旋转后就结束循环。
由上述的分析我们就可以单独的写出一个更新平衡因子的函数,然后将其插入每次的删除中。
void UpdateBF(node * parent,node *cur)
{
while (parent)
{
if (parent->_left == cur)
parent->_bf++;
else
parent->_bf--;
if (parent->_bf == 1 || parent->_bf == -1)
{
return;
}
else if (parent->_bf == 0)
{
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || 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)
{
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
cur = parent;
parent = parent->_parent;
}
else
{
assert(false);
}
}
}
最终我们的删除函数如下。
bool erase(T k)
{
//空树
if (_root == nullptr)
return false;
node* cur = find(k);
node* parent = cur->_parent;
if (cur == nullptr)
return false;
//一个孩子
if (cur->_left == nullptr)
{
//根节点特殊处理
if (parent == nullptr)
{
_root = cur->_right;
delete cur;
return true;
}
if (parent->_left == cur)
{
parent->_left = cur->_right;
cur->_right->_parent = parent;
}
else
{
parent->_right = cur->_right;
cur->_left->_parent = parent;
}
//更新平衡因子
UpdateBF(parent, cur);
delete cur;
}
else if (cur->_right == nullptr)
{
//根节点特殊处理
if (parent == nullptr)
{
_root = cur->_left;
delete cur;
return true;
}
if (parent->_left == cur)
{
parent->_left = cur->_left;
cur->_left->_parent = parent;
}
else
{
parent->_right = cur->_left;
cur->_left->_parent = parent;
}
//更新平衡因子
UpdateBF(parent, cur);
delete cur;
}
//删除两个结点
//找到左子树最大值
node* subL = cur->_left;
node* subParentMax = cur;
while (subL->_right)
{
subParentMax = subL;
subL = subL->_right;
}
//删除
cur->_key = subL->_key;
if (subParentMax->_left == subL)
subParentMax->_left = nullptr;
else
subParentMax->_right = subL->_left;
//更新平衡因子
UpdateBF(subParentMax, subL);
delete subL;
return true;
}