注意:此篇文章只是详细介绍文章数据结构——实现一个二叉查找树(BST)实现的二叉树的删除操作,所以一些类的定义并不在本文中。
**删除节点分类**
:
- 删除的结点是叶子节点
需要做两件事:
- 断开和父节点的连接
- 释放删除节点的内存
由于这种情况很简单,不做说明,可参见代码。
- 删除的结点不是叶子节点
此时删除的节点必定有左子树或右子树或者两者兼具。
以图1的二叉树为例,我们可以有两种做法。就以删除节点3为例,即可以将节点3左子树的最大值
节点补在节点3的位置,也可以将节点3右子树的最小值
节点补在节点3的位置,如图2。
为什么是怎样呢?也很好理解,为了满足二叉树的特性,我们必定找挨着删除节点的节点,如下图,可以把BST映射到一个数轴上理解,这样就有大小两种情况,即3节点左边的最大值,或右边的最小值。
好了,就下来我们的目标就很明确了:先找到删除节点的左子树的最大值节点,再找到右子树的最小值节点
,使用两者中的一个就行。当然如果被删除的节点没有左子树(或右子树),就只能使用右子树的最小值节点(或左子树的最大值节点)。
假设我们删除上面图1地节点3,现在我们已近找到了补在被删除节点3位置的节点4了,接下来我们需要做什么呢?这里先给出结论再论述:
- 将节点4的子树连接在节点4的父节点上
- 将节点4补在节点3位置
- 将节点4的左右孩子节点指向节点3的左右孩子的指向
这看上去简单的三步,实际操作却不是那么简单。接下来论述这三步。
第一步:将节点4的子树连接在节点4的父节点上
既然要连接在父节点上,(1)那么我们必须先找到节点4的父节点。然后(2)将节点4的子树连接在节点4的父节点上。
第二步:将节点4补在节点3位置
同样的,我们也需要找到被删除的节点3的父节点,这样才能将节点4连接和其连接起来
第三步:将节点4的左右孩子节点指向节点3的左右孩子的指向
、
这没什么好说的,毕竟如果不连接上,就丢掉了节点。
下面,我直接贴出代码,我按照思路对代码进行说明。
template <typename Type>
void Bst<Type>::Delete(const Element<Type>& _data)
{
BstNode<Type> *deleteNode = nullptr;
BstNode<Type> *deleteNodeParent = nullptr;
BstNode<Type> *minNode = nullptr;
BstNode<Type> *maxNode = nullptr;
deleteNode = Search(_data);
if (!deleteNode) return;//deleteNode为空,表明BST中没有元素为_data的节点,直接返回
if (deleteNode->leftChild){//deleteNode有左子树,得到deleteNode左子树的最大值节点
maxNode = FindMax(deleteNode->leftChild);
}
if (deleteNode->rightChild){//deleteNode有右子树,得到deleteNode右子树的最小值节点
minNode = FindMin(deleteNode->rightChild);
}
deleteNodeParent = FindParent(deleteNode);//找到deleteNode节点的父节点
//maxNode和minNode的节点为空,表明deleteNode是一个叶子节点
if (!maxNode && !minNode){
if (deleteNodeParent->leftChild == deleteNode)
deleteNodeParent->leftChild = nullptr;
else
deleteNodeParent->rightChild = nullptr;
delete deleteNode;
}
//表明deleteNode最少有一个子树不空
else{
//将tmp节点的父节点和tmp的子节点连接
BstNode<Type> *tmp = (maxNode == nullptr) ? minNode : maxNode;
BstNode<Type> * tmpParent = FindParent(tmp);
if (tmp->leftChild){//左孩子不空,右孩子必空.此情况对应的是 maxNode
if (tmpParent->leftChild == tmp)//tmp是左孩子
tmpParent->leftChild = tmp->leftChild;
else
tmpParent->rightChild = tmp->leftChild;
}
else{//右孩不空,左孩子必空,此情况对应的是 minNode
if (tmpParent->leftChild == tmp)
tmpParent->leftChild = tmp->rightChild;
else
tmpParent->rightChild = tmp->rightChild;
}
if (deleteNodeParent == nullptr)
root = tmp;
else{
if (tmp->content.key < deleteNodeParent->content.key)
deleteNodeParent->leftChild = tmp;
else
deleteNodeParent->rightChild = tmp;
}
tmp->leftChild = deleteNode->leftChild;
tmp->rightChild = deleteNode->rightChild;
delete deleteNode;
}
}
最开始,我定义了4个节点类型的变量:
- deleteNode :被删除的节点
- deleteNodeParent :被删除节点的父节点
- minNode:被删除节点的右子树的最小值节点
- maxNode:被删除节点的左子树的最大值节点
接着调用search()函数,找到需要删除的节点,将值赋给deleteNode。若为空,直接退出函数,说明没有匹配到删除的节点。
接下来就是找到deleteNode节点的左右子树的最大值节点和最小值节点了。当然,deleteNode节点不一定有左右子树,所以我们在做的时候给了if条件判断:if (deleteNode->leftChild)
和if (deleteNode->rightChild)
。通过FindMax和FindMin这两个函数分别找到deleteNode节点的左右子树的最大值节点和最小值节点,并将它们分别赋给maxNode和minNode两个变量。
到此,我们就找到需要被删除的节点deleteNode;被删除的节点deleteNode左子树的最大值节点maxNode和被删除的节点deleteNode右子树最小值节点minNode。
在前面我们将删除情况分成了两类,第一种情况是删除叶子节点,对应了if (!maxNode && !minNode)
条件下的语句块。此语句块就干了前面描述的两件事:断开和父节点连接和释放内存。这里注意:断开和父节点连接时,我们需要判断它是左孩子节点还是右孩子节点
。
下面着重描述第二种情况,既删除的是非叶子节点,它对应的是代码中匹配if (!maxNode && !minNode)语句的else语句块
。
前面我们提到了需要做的3步:
- 将节点4的子树连接在节点4的父节点上
- 将节点4补在节点3位置
- 将节点4的左右孩子节点指向节点3的左右孩子的指向
else语句块做的就是这三步。下面开始描述:
首先,我们执行了这么一句:
BstNode<Type> *tmp = (maxNode == nullptr) ? minNode : maxNode;
因为我们只需要maxNode和minNode中的一个,所以这里做了一个选择,它的含义就是,如果maxNode为空(对应deleteNode没有左子树,有右子树),我们就用minNode来补在deleteNode位置;如果maxNode不空(deleteNode可能有也可能没有右子树),用maxNode来补在deleteNode位置。此时,tmp就是补deleteNode位置的节点变量
。
第一步:我们需要将tmp的子树连接在tmp的父节点上,对应代码如下:
BstNode<Type> * tmpParent = FindParent(tmp);
if (tmp->leftChild){//左孩子不空,右孩子必空.此情况对应的是 maxNode
if (tmpParent->leftChild == tmp)//tmp是左孩子
tmpParent->leftChild = tmp->leftChild;
else//tmp是右孩子
tmpParent->rightChild = tmp->leftChild;
}
else{//右孩不空,左孩子必空,此情况对应的是 minNode
if (tmpParent->leftChild == tmp) //tmp是左孩子
tmpParent->leftChild = tmp->rightChild;
else//tmp是右孩子
tmpParent->rightChild = tmp->rightChild;
}
调用成员函数FindParent来获得tmp的父节点,将值赋给 tmpParent。
若tmp->leftChild不空,此时tmp取得是maxNode的值,试想一下,如果是minNode,而leftChild不空,minMax还是最小值节点吗?肯定不是啊!tmp->rightChild不空同理。
如果tmp是其父节点tmpParent的左孩子(tmpParent->leftChild == tmp),就将tmp的子树(需要区分左右)接在tmpParent左孩子上,否则接在右孩子上。注意:这里是用tmp的左子树还是右子树连,就需要看tmp是maxNode还是minNode
。
第二步:将tmp补在deleteNode处,对应代码如下:
if (deleteNodeParent == nullptr)
root = tmp;
else{
if (tmp->content.key < deleteNodeParent->content.key)
deleteNodeParent->leftChild = tmp;
else
deleteNodeParent->rightChild = tmp;
}
若是deleteNodeParent == nullptr,表明需要删除的节点deleteNode是根节点,将tmp作为根即可。否则根据key值来确定tmp是作为deleteNodeParent的左孩子还是右孩子。
第三步:将tmp的左右孩子指向deleteNode的左右孩子并释放deleteNode内存,对应代码如下:
tmp->leftChild = deleteNode->leftChild;
tmp->rightChild = deleteNode->rightChild;
delete deleteNode;
好了,到这里BST的删除就算是做完了。关于代码的测试,我在数据结构——实现一个二叉查找树(BST)中详细介绍了,本篇文章只是因为删除操作较为麻烦所以单独详细描述。
当然,上述代码我简单测试了是没问题,但无法保证代码完美,如果有代码有问题的话,欢迎各位批评指正;如是有疑问的话,欢迎在评论区留言,看到了我会第一时间回复。