前言
最近在学习数据结构,学到了二叉搜索树节点的删除那里。左思右想除了老师写的用递归函数来实现,其他方法怎么实现二叉搜索树节点删除,思考了几天没头绪,无奈只能查查相关资料。
注:(原贴在这里20张图带你彻底搞懂二叉树的删除操作)本帖讲的是自己对原贴的重新理解,代码其实是一样的只是稍微做了修改。原贴使用的是JS,这里使用的是C/C++。如有冒犯请告知删除!
【想直接复制粘贴完整的二叉搜索树节点删除源码直接往下拉到底】
二叉搜索树删除节点思路
把将要删除的节点的值,与节点 root(根节点) 进行比较,若小于则去到左子树进行比较,若大于则去到右子树进行比较,重复以上操作直到找到一个节点的值等于删除的值,则将此节点删除。
如(图2)所示假设删除的是节点11,根节点是19,根节点19和根节点左子树上的右边的孩子节点11进行比较,若小于19则去根节点的左子树上找,找到了一个节点7,节点7又跟被删节点11进行比较……,直到找到的节点的值和删除节点的值相等为止。找根节点19右边子树上操作也是一样。
(为什么要这样操作呢?因为根据二叉搜索树的特性,若根节点的左子树不为空,根节点左边的孩子节点的值必须小于根节点的值,若根节点的右子树不为空,根节点右边的孩子节点的值必须大于根节点的值,如果有根节点的孩子的孩子节点,也就是孙子节点,同理也是一样的)
算法思路:因为要不断的寻找,这里可以使用一个while循环,将条件设成找到的节点不为空,不断的在左右子树上找被删节点的值,为空的话,就说明找到要删除的节点了。可以定义一个指针,开始指向根节点,每次while循环遍历,指向根节点的指针就指向它的左边的孩子节点或右边的孩子节点,直到找到被删的节点,然后就把这个指针赋值为NULL。
这里还要再定义个指针,指向被删节点的父节点,因为被删节点删除后,被删节点它如果有孩子节点的话,这个指针是要指向被删节点的孩子节点。同时再定义个bool类型,表示被删节点的父节点它是在左节点还是在右节点上,这点很重要下面会详说。最后别忘了开始分配的内存到最后一定要手动释放掉,不管是C还是C++,不考虑智能指针的话。
bool DeleteNode(Bnode* root, int key) {
if (!root || !key)return false;
Bnode* current = root;//当前节点,从根节点开始往下找
Bnode* current_parent = NULL; //当前节点的父节点
Bnode* delNode = NULL;//要删除的节点
bool isLeft = true;//记录最后是父节点的左还是右节点(true表示左,false表示右)
//寻找要删除的左子或右子节点
while (current) {
if (current->data == key) {
delNode = current;
current = NULL;
}
else if (current->data > key) { //去当前节点的左子节点找
isLeft = true;
current_parent = current;
current = current->lchild;
}
else if (current->data < key) {//去当前节点的右子节点找
isLeft = false;
current_parent = current;
current = current->rchild;
}
}
}
删除节点时有4种情况须分别处理:
1. 删除节点不存在左右子节点,即为叶子节点,直接删除
就是下面(图3)的情况
这个算法思路很简单就是判断被删节点它的左和右的孩子节点是否都为NULL,都为NULL表达它就是一个叶子节点。
还记得之前定义的bool类型嘛,前面只是简单的说了一下它的作用是找到被删节点的父节点是在左还是右,其实就是方便处理操作被删节点的后事和被删节点它下面的节点。通过bool类型的值,true或false就能很清楚的确定被删节点的父节点是在左边方向还是右边方向,假设如果删除的是叶子节点,我们只需通过bool类型的值,确定被删节点的父节点它的孩子节点方向把被删节点的父节点的左或右子节点赋值NULL就可以了,如果删除的不是叶子节点请看下面的第2种和3种情况。
//【1.如果删除的是叶子节点,即两端没有节点】
if (delNode->lchild == NULL &&
delNode->rchild == NULL) {
if (delNode == root) {
delete root;//如果开始就只有一个节点,即根节点,直接delete
}
else {
if (isLeft) {
current_parent->lchild = NULL;
delete delNode;
}
else {
current_parent->rchild = NULL;
delete delNode;
}
}
2.删除节点存在左子节点,不存在右子节点,直接把左子节点替代删除节点。
删除节点存在右子节点,不存在左子节点,直接把右子节点替代删除节点。
删除节点存在左子节点,不存在右子节点,直接把左子节点替代删除节点
上面(图4)所示
删除节点存在右子节点,不存在左子节点,直接把右子节点替代删除节点
上面(图5)所示
算法思路:这里其实和上面的搜索叶子节点有点类似,叶子节点是两边都为NULL,这里是单边为NULL。但需要注意的是,它们下面都有它们的孩子节点,一旦被删的节点删除后,被删节点的孩子节点要连到被删节点的父节点上面,如(图4)如(图5)所示。
下面出现的两种情况,须分别处理
(1.)判断被删节点是否是根节点,如果是下面(图6)情况
如果被删节点就是根节点,那么指向根节点的指针,就该指向根节点右边的孩子节点或者是左边的孩子节点
(2.)删除节点,它的左边节点为NULL,右边节点不为NULL 【下面(图7)所示 删除【节点5】为例子】
删除节点,它的右边节点为NULL,左边节点不为NULL 【下面(图7)所示 删除【节点11】为例子】
不是根节点情况,如(图7)。我们前面说了,通过bool类型赋值的true还是false,就能找到被删节点的父节点它的左子节点或右子节点方向, 找到后看被删节点它的孩子节点是否存在它的节点左边或右边,在左边节点的话被删节点的父节点左子节点或右子节点指向被删节点它的孩子节点左边,在右边节点的话就指向被删节点它的孩子节点右边。
//[2.如果删除的节点只有一端节点,即在左或右边的节点]
}else if (delNode->lchild == NULL) {//左节点为空,右节点有值,右端有节点
if (delNode == root) { //如果是根节点
root = delNode->rchild;
delete delNode;
}
else {
if (isLeft) { //左节点
current_parent->lchild = delNode->rchild;
delete delNode;
}
else {
current_parent->rchild = delNode->rchild;
delete delNode;
}
}
}
else if (delNode->rchild == NULL) {//右节点为空,左节点有值,左端有节点
if (delNode == root) { //如果是根节点
root = delNode->lchild;
delete delNode;
}
else {
if (isLeft) {
current_parent->lchild = delNode->lchild;
delete delNode;
}
else {
current_parent->rchild = delNode->lchild;
delete delNode;
}
}
}
3.删除节点存在左右子节点,取左子树上的最大节点或右子树上的最小节点替换删除节点【这里是取右子树上最小节点替代被删节点】 下面(图8)所示
如(图8)所示,要想删除节点25,就必须找到它的替代节点,前面说了,删除节点存在左右子节点,取左子树上的最大节点或右子树上的最小节点替换删除节点,这里我们可以写一个函数,来专门寻找被删节点它的替代节点。注:这里的前驱节点就是被删节点最大节点,后继节点就是被删节点最小节点。
还有就是我们还要再写一个寻找被删节点的后继节点的父节点的函数,为什么后面详细讲。
//找被删除节点的前驱节点
Bnode* getSuccessPre(Bnode* delNode) {// 传入被删节点
Bnode* preNode = delNode->lchild;
while (preNode->rchild) {
preNode = preNode->rchild;
}
return preNode;
}
//找被删除节点的前驱节点的父节点
Bnode* Pre_parent(Bnode* delNode) {
Bnode* rearNode = delNode->lchild;
Bnode* Preparent = delNode;
while (rearNode->rchild) {
Preparent = rearNode;
rearNode = rearNode->rchild;
}
return Preparent;
}
//找被删除节点的后继节点
Bnode* getSuccessrear(Bnode* delNode) {// 传入被删节点
Bnode* rearNode= delNode->rchild;
while (rearNode->lchild) {
rearNode = rearNode->lchild;
}
return rearNode;
}
//找被删除节点的后继节点的父节点
Bnode* Rear_parent(Bnode* delNode) {
Bnode* rearNode = delNode->rchild;
Bnode* Preaparent = delNode;
while (rearNode->lchild) {
Preaparent = rearNode;
rearNode = rearNode->lchild;
}
return Preaparent;
}
找到被删节点的替代节点后,这里有3种情况需要分别处理
(1.)被删节点就是后继节点的父节点
(图9)
被删节点是节点11,它的后继节点是节点15,而节点15的父节点是节点11。
这种情况就是被删节点就是后继节点的父节点。
还是老的套路,先通过之前定义的bool类型赋值的true或false,确定被删节点的父节点的左子或右子节点的方向位置,然后被删节点的父节点指向被删节点的后继节点,被删节点的后继节点左子节点指向被删节点的左子节点。
被删节点既是后继节点的父节点也是根节点。
还有一种情况一般也会出现,就是二叉搜索树节点很少的情况,少的只有三个节点即只有根节点,左子节点和右子节点。
如下面(图10)所示
(图10)的这种情况它即是被删节点的后继节点的父节点,也是根节点。需要注意的是,如果是这种情况,那么原先指向根节点的指针,就该指向被删节点的后继节点。被删节点的后继节点左子节点指向被删节点的左子节点。
(2.)被删节点就是根节点 如下面(图11)所示
这种情况需要注意的是,如果是像(图11)那样茂盛的二叉搜索树,删除根节点,那么被删节点的后继节点左子节点一定是不存在的,因为如果存在的话,那么现在找到的后继节点一定会改变,会变成被删节点的后继节点的左子节点的那个节点。
如下面(图12)的这种情况就是被删节点是根节点,而它的后继节点左子节点有值。
还记得上面说的那个寻找被删节点的后继节点的父节点的函数嘛,它的作用就是,当被删节点是根节点的情况下如(图12)的情况,当被删节点的后继节点有孩子节点,被删节点的后继节点的父节点要指向被删节点的后继节点的孩子节点。
通过被删节点的后继节点左子节点不存在的规律,这里就可以直接写死,被删节点的后继节点的父节点的左子节点指向被删节点的后继节点右子节点,然后被删节点的后继节点左子节点和右子节点指向被删节点的左子节点和右子节点。
(3.)被删节点既不是后继节点的父节点也不是根节点
通过寻找后继节点函数找到被删节点的后继节点,bool类型找到被删节点的父节点在左子节点方向上还是在右子节点的方向上,然后被删节点的父节点左子节点或右子节点指向被删节点的后继节点,
套路和上面的一样这里就复制粘贴了:通过被删节点的后继节点左子节点不存在的规律,这里就可以直接写死,被删节点的后继节点的父节点的左子节点指向被删节点的后继节点右子节点,然后被删节点的后继节点左子节点和右子节点指向被删节点的左子节点和右子节点。
下面是完整的二叉搜索树删除节点源码,可自行复制粘贴进行调试。
bool DeleteNode(Bnode* root, int key) {
if (!root || !key)return false;
Bnode* current = root;//当前节点,从根节点开始往下找
Bnode* current_parent = NULL; //当前节点的父节点
Bnode* delNode = NULL;//要删除的节点
bool isLeft = true;//记录最后是父节点的左还是右节点(true表示左,false表示右)
//寻找要删除的左子或右子节点
while (current) {
if (current->data == key) {
delNode = current;
current = NULL;
}
else if (current->data > key) { //去当前节点的左子节点找
isLeft = true;
current_parent = current;
current = current->lchild;
}
else if (current->data < key) {//去当前节点的右子节点找
isLeft = false;
current_parent = current;
current = current->rchild;
}
}
//删除的节点不存在,没有找到该节点
if (!delNode)return false;
//【1.如果删除的是叶子节点,即两端没有节点】
if (delNode->lchild == NULL &&
delNode->rchild == NULL) {
if (delNode == root) {
delete root;//如果开始就只有一个节点,即根节点,直接delete
}
else {
if (isLeft) {
current_parent->lchild = NULL;
delete delNode;
}
else {
current_parent->rchild = NULL;
delete delNode;
}
}
//[2.如果删除的节点只有一端节点,即在左或右边的节点]
}else if (delNode->lchild == NULL) {//左节点为空,右节点有值,右端有节点
if (delNode == root) { //如果是根节点
root = delNode->rchild;
delete delNode;
}
else {
if (isLeft) { //左节点
current_parent->lchild = delNode->rchild;
delete delNode;
}
else {
current_parent->rchild = delNode->rchild;
delete delNode;
}
}
}
else if (delNode->rchild == NULL) {//右节点为空,左节点有值,左端有节点
if (delNode == root) { //如果是根节点
root = delNode->lchild;
delete delNode;
}
else {
if (isLeft) {
current_parent->lchild = delNode->lchild;
delete delNode;
}
else {
current_parent->rchild = delNode->lchild;
delete delNode;
}
}
}
else { //[3.两端都有节点]
//被删节点的后继节点
Bnode* rearNode = getSuccessrear(delNode);
//被删节点的后继节点的父节点
Bnode* rearparent = Rear_parent(delNode);
if (delNode == rearparent) {//后继节点的父节点是被删节点
if (delNode == root) {// 如果是根节点
root = rearNode;
}
else if(isLeft){
current_parent->lchild = rearNode;
}
else {
current_parent->rchild = rearNode;
}
rearNode->lchild = delNode->lchild;
delete delNode;
}
else {//后继节点父节点不是被删节点
if (delNode == root) {//如果是根节点
root = rearNode;
}
else if (isLeft) {//根据被删节点在父节点的位置选择合适的位置进行替换
current_parent->lchild = rearNode;
}
else {
current_parent->rchild = rearNode;
}
rearparent->lchild = rearNode->rchild;
rearNode->lchild = delNode->lchild;
rearNode->rchild = delNode->rchild;
delete delNode;
}
}
return true;
}