一棵搜索二叉树的结点的度有以下三种情况:0 1 2
那我们就需要依据节点不同的度进行不同的讨论
假设被删除的节点是node,我们仍然需要考虑该node是否为根节点,因为根节点无双亲结点,其索引只有一个root指针
情况一:node没有孩子,是叶子结点,度为0,直接free该节点的分配空间
(1) node不是根节点,直接把node删除,把node父亲对应的孩子指针置空
(2) node是根节点,直接删除,把根指针置空
情况二:node只有一个孩子节点,节点度为1
(1) node不是根节点,并且node只有一个孩子,就让这个孩子节点顶替node的位置,也就是node的parent的指向node的指针,要去指向该孩子
(2) node是根节点,让根指针直接指向node的唯一孩子即可,当然了先需要移动root指针,另外将原根节点free
情况三:node有两个孩子,节点度为2
我们从排序的中序序列来观察一下
假如该序列的顺序排序为:1 3 4 6 7 8 10 13 14,且该树的根节点为8,如果删除节点8的话,新树的排序为:
1 3 4 6 7 10 13 14
那么要么我们将节点7移到8的位置
或者10代替8向前移
都可以解决该问题,7是左子树中最靠右的节点,10是右子树中的根节点,是原节点8的左膀右臂
法1:让node节点的左子树中的最大值x来替换node节点的值,转化为删除x所在节点:如果x是叶子结点转化为情况一,如果x不是叶子结点转化为情况二(x只有左孩子) x是node左子树最靠右的节点
法2:让node节点的左子树中的最小值x来替换node节点的值,转化为删除x所在节点:如果x是叶子结点转化为情况一,如果x不是叶子结点转化为情况二(x只有右孩子) x是node右子树最靠左的节点
所以综上所述
①.对于叶子结点的删除,保存其父亲节点位置后,直接删除该叶子结点,令其父亲节点指向它的指针指向NULL;
②.对于度为1的节点删除,保存该节点的父亲节点位置后,另其父亲节点指向该节点的指针,指向其唯一的左/右孩子,然后删除该节点即可;
(针对以上两个类型的节点,我们都需要判断其是否为根节点,如果是根节点就要做根指针的替换,如指向NULL或者换成其对应的左/右孩子)
③.对于度为2的节点删除,通过分析我们知道了,我们需要找的是度为2的节点,在中序序列中的,前驱和后继,用以替换其位置,因为其本身具有特殊性,不能像度为0或者1的节点一样直接进行节点的删除和双亲结点指针的变换,以满足新的二叉搜索树仍然保持一个顺序序列。
所以我们通过寻找其前驱后继结点进行替换的方法,先进行数据上的整体替换,达到顺序序列的要求,然后再对被替换的(此时被替换结点成为了多余节点,即需要删除节点)进行删除操作,仍然是分析该节点的情况,其度为0或1,然后对应节点度进行情况一、二的结点的修正,完成一个节点的删除。
下面是代码操作:
//删除值为k的节点
void delete(int key){
Node *node = root;
Node *pre = NULL;
if(node!=NULL){
while(node->data != key){
pre = node;
if(key < node->data){
node = node->left;
}
else if(key > node->data){
node = node->right;
}
}
if(node->left == NULL && node->right == NULL){//情况一,度为0的节点
if(pre==NULL){//node的父亲为空,那么node为根节点
root = NULL;
free(node);
node = NULL;//野指针
}
else{
if(pre->left == node){
pre->left = NULL;
free(node);
node == NULL;
}
else{
pre->right == NULL;
free(node);
node = NULL;
}
}
}
//情况二-1
else if(node->left != NULL && node->right == NULL){
if(pre == NULL){
root = node->left;
node->left = NULL;
free(node);
node = NULL;
}
else{
if(node == pre->left){
pre->left = node->left;
node->left = NULL;
free(node);
node = NULL;
}
else{
pre->right = node->left;
node->left = NULL;
free(node);
node = NULL;
}
}
}
//情况二-2
else if(node->left == NULL && node->right != NULL){
if(pre == NULL){
root = node->right;
node->right = NULL;
free(node);
node = NULL;
}
else{
if(node == pre->left){
pre->left = node->right;
node->right = NULL;
free(node);
node = NULL;
}
else{
pre->right = node->right;
node->right = NULL;
free(node);
node = NULL;
}
}
}
//情况三
else{
Node *x = node->left;//这里使用上述的方法一,找左子树的最右节点
Node *prex = node;//节点x的父亲节点,用于改变节点指针连通和赋值
while(x->right!=NULL){
prex = x;
x = x->right;//一直往右走,中序序列就是这样的
}
node->data = x->data;
if(prex == node){
//如果prex是根节点,即node没有右子树,只有左子树,找到的最右节点就是x,即node->left
node->left = x->left;
free(x);
x = NULL;
}
else{
//存在右子树也找到了右子树中的最右节点x
prex->right = NULL;
free(x);
x = NULL;
}
}
}
}
上述代码为了描述更直观,将左右孩子节点的命名改为了left和right,但是为了操作方便和上一个二叉搜索树的代码对接,这里再统一改一下命名(视频里上下节老师的存储结构设置的不一样)
//删除值为k的节点
void delete(int key){
Node *node = root;
Node *pre = NULL;
if(node!=NULL){
//寻找节点
while(node->data != key){
pre = node;
if(key < node->data){
node = node->l;
}
else if(key > node->data){
node = node->r;
}
if(node==NULL){
printf("查无此数据\n");
return;
}
}
//编译异常说明node指向空后无值,无法调用(如果不加最后的if判断的话)
if(node->l == NULL && node->r == NULL){
//情况一,度为0的节点
if(pre==NULL){
//若node的父亲为空,那么node为根节点
root = NULL;
free(node);
node = NULL;//野指针
}
else{
//pre不为空,说明找到了对应的node->data == key
if(pre->l == node){
//还要判断node是pre节点的左孩子还是右孩子
pre->l = NULL;
free(node);
node == NULL;
}
else{
pre->r == NULL;
free(node);
node = NULL;
}
}
}
//情况二-1 找到的node,node的度为1,找到了该节点的左孩子非空,没有右孩子
else if(node->l != NULL && node->r == NULL){
if(pre == NULL){
//仍然是判断是否删除的为根节点
root = node->l;
node->l = NULL;
free(node);
node = NULL;
}
else{
//再次判断node节点属于是pre的左孩子还是右孩子
if(node == pre->l){
pre->l = node->l;
node->l = NULL;
//针对node的左孩子的继承(pre->left = node->left)
free(node);
node = NULL;
}
else{
pre->r = node->l;
//针对node的左孩子的继承(pre->right = node->left)
node->l = NULL;
free(node);
node = NULL;
}
}
}
//情况二-2找到的node,node的度为1,找到了该节点的右孩子非空,没有左孩子
else if(node->l == NULL && node->r != NULL){
//下面的代码和上面的内容基本相似
if(pre == NULL){
root = node->r;
node->r = NULL;
free(node);
node = NULL;
}
else{
if(node == pre->l){
pre->l = node->r;
node->r = NULL;
free(node);
node = NULL;
}
else{
pre->r = node->r;
node->r = NULL;
free(node);
node = NULL;
}
}
}
//情况三 找到的node节点的度为2 使用方法一处理该内容 即寻找node左子树的最右节点
else{
//这里设置新的指针进行赋值是必要的,因为要保留node节点的位置,删除的是其代替的节点
Node *x = node->l;//这里使用上述的方法一,找左子树的最右节点
Node *prex = node;//节点x的父亲节点,用于改变节点指针连通和赋值
while(x->r!=NULL){
prex = x;
x = x->r;//一直往右走,中序序列就是这样的
}
node->data = x->data;
if(prex == node){
//如果prex是根节点,即node没有右子树,只有左子树,找到的最右节点就是x,即node->left
node->l = x->l;
free(x);
x = NULL;
}
else{
//存在右子树也找到了右子树中的最右节点x
prex->r = NULL;
free(x);
x = NULL;
}
}
}
}
然后把它再加入到二叉搜索树的整体代码中,回顾“二叉排序树概述及其插入操作”章节末代码
#include <stdio.h>
#include <stdlib.h>
//树节点
typedef struct BTNode{
struct BTNode *l;
struct BTNode *r;
int data;
}Node;
Node *root = NULL;//根指针
int SearchKey(int key){
Node *p = root;
//不使用递归
while(p!=NULL){
if(p->data==key){
//找到key,返回1
return 1;
}
else if(p->data > key){
p = p->l;
}
else{
p = p->r;
}
}
//没有找到key,返回0
return 0;
}
//插入一个数据x
void insert(int x){
Node *p = root;
Node *pre = NULL;//一直指向p的双亲结点
while(p!=NULL){
//通过while循环找到插入位置
pre = p;
if(x < p->data){
p = p->l;
}
else if(x > p->data){
//这里必须使用else if,因为在这个while代码块中我们一直在变动指针p的位置,如果再进行x的判断可能会出现p为NULL的情况
p = p->r;
}
}
//核心仍然是查找
if(x < pre->data){
pre->l = (Node*)malloc(sizeof(Node));
pre->l->data = x;
pre->l->l = NULL;
pre->l->r = NULL;
}
//这里使用if或者else if就没什么差别了,用了else少进行一次判断,因为这里的pre不会再发生改变
else if(x > pre->data){
pre->r = (Node*)malloc(sizeof(Node));
pre->r->data = x;
pre->r->l = NULL;
pre->r->r = NULL;
}
}
//中序遍历二叉搜索树
void inshow(Node *root)
{
if(root==NULL){
return;
}
if(root->l != NULL){
inshow(root->l);
}
printf("%d ",root->data);
if(root->r != NULL){
inshow(root->r);
}
}
//先初始化一个根节点用于后续操作
void initTree(int key){
root = (Node*)malloc(sizeof(Node));
root->data = 3;
root->l = NULL;
root->r = NULL;
}
//删除值为k的节点
void deleteNode(int key){
Node *node = root;
Node *pre = NULL;
if(node!=NULL){
//寻找节点
while(node->data != key){
pre = node;
if(key < node->data){
node = node->l;
}
else if(key > node->data){
node = node->r;
}
if(node==NULL){
printf("查无此数据\n");
return;
}
}
//编译异常说明node指向空后无值,无法调用(如果不加最后的if判断的话)
if(node->l == NULL && node->r == NULL){
//情况一,度为0的节点
if(pre==NULL){
//若node的父亲为空,那么node为根节点
root = NULL;
free(node);
node = NULL;//野指针
}
else{
//pre不为空,说明找到了对应的node->data == key
if(pre->l == node){
//还要判断node是pre节点的左孩子还是右孩子
pre->l = NULL;
free(node);
node == NULL;
}
else{
pre->r == NULL;
free(node);
node = NULL;
}
}
}
//情况二-1 找到的node,node的度为1,找到了该节点的左孩子非空,没有右孩子
else if(node->l != NULL && node->r == NULL){
if(pre == NULL){
//仍然是判断是否删除的为根节点
root = node->l;
node->l = NULL;
free(node);
node = NULL;
}
else{
//再次判断node节点属于是pre的左孩子还是右孩子
if(node == pre->l){
pre->l = node->l;
node->l = NULL;
//针对node的左孩子的继承(pre->left = node->left)
free(node);
node = NULL;
}
else{
pre->r = node->l;
//针对node的左孩子的继承(pre->right = node->left)
node->l = NULL;
free(node);
node = NULL;
}
}
}
//情况二-2找到的node,node的度为1,找到了该节点的右孩子非空,没有左孩子
else if(node->l == NULL && node->r != NULL){
//下面的代码和上面的内容基本相似
if(pre == NULL){
root = node->r;
node->r = NULL;
free(node);
node = NULL;
}
else{
if(node == pre->l){
pre->l = node->r;
node->r = NULL;
free(node);
node = NULL;
}
else{
pre->r = node->r;
node->r = NULL;
free(node);
node = NULL;
}
}
}
//情况三 找到的node节点的度为2 使用方法一处理该内容 即寻找node左子树的最右节点
else{
//这里设置新的指针进行赋值是必要的,因为要保留node节点的位置,删除的是其代替的节点
Node *x = node->l;//这里使用上述的方法一,找左子树的最右节点
Node *prex = node;//节点x的父亲节点,用于改变节点指针连通和赋值
while(x->r!=NULL){
prex = x;
x = x->r;//一直往右走,中序序列就是这样的
}
node->data = x->data;
if(prex == node){
//如果prex是根节点,即node没有右子树,只有左子树,找到的最右节点就是x,即node->left
node->l = x->l;
free(x);
x = NULL;
}
else{
//存在右子树也找到了右子树中的最右节点x
prex->r = NULL;
free(x);
x = NULL;
}
}
}
}
//主函数
int main()
{
initTree(3);
inshow(root);
printf("\n");
insert(1);
inshow(root);
printf("\n");
insert(2);
inshow(root);
printf("\n");
insert(6);
inshow(root);
printf("\n");
insert(5);
inshow(root);
printf("\n");
insert(4);
inshow(root);
printf("\n");
deleteNode(5);
inshow(root);
printf("\n");
return 0;
}
可以通过在main函数中调用函数实现一下各种功能,可自行添加代码用于测试
上述程序运行结果为:
3
1 3
1 2 3
1 2 3 6
1 2 3 5 6
1 2 3 4 5 6
1 2 3 4 6