数据结构——详解二叉查找树(BST)的删除操作

注意:此篇文章只是详细介绍文章数据结构——实现一个二叉查找树(BST)实现的二叉树的删除操作,所以一些类的定义并不在本文中。

**删除节点分类**

  1. 删除的结点是叶子节点
    需要做两件事:
  • 断开和父节点的连接
  • 释放删除节点的内存
    由于这种情况很简单,不做说明,可参见代码。
  1. 删除的结点不是叶子节点
    此时删除的节点必定有左子树或右子树或者两者兼具。

以图1的二叉树为例,我们可以有两种做法。就以删除节点3为例,即可以将节点3左子树的最大值节点补在节点3的位置,也可以将节点3右子树的最小值节点补在节点3的位置,如图2。
图1

图1

在这里插入图片描述

图2

为什么是怎样呢?也很好理解,为了满足二叉树的特性,我们必定找挨着删除节点的节点,如下图,可以把BST映射到一个数轴上理解,这样就有大小两种情况,即3节点左边的最大值,或右边的最小值。
在这里插入图片描述

好了,就下来我们的目标就很明确了:先找到删除节点的左子树的最大值节点,再找到右子树的最小值节点,使用两者中的一个就行。当然如果被删除的节点没有左子树(或右子树),就只能使用右子树的最小值节点(或左子树的最大值节点)。

假设我们删除上面图1地节点3,现在我们已近找到了补在被删除节点3位置的节点4了,接下来我们需要做什么呢?这里先给出结论再论述:

  1. 将节点4的子树连接在节点4的父节点上
  2. 将节点4补在节点3位置
  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步:

  1. 将节点4的子树连接在节点4的父节点上
  2. 将节点4补在节点3位置
  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)中详细介绍了,本篇文章只是因为删除操作较为麻烦所以单独详细描述。

当然,上述代码我简单测试了是没问题,但无法保证代码完美,如果有代码有问题的话,欢迎各位批评指正;如是有疑问的话,欢迎在评论区留言,看到了我会第一时间回复。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值