学习随记四十二——B树(递归实现版——三、我的错误删除操作详解)


前言

在我反思递归实现插入的时候发现了我设计时没有考虑到的一个致命的问题栈溢出!!!,这是我在设计之初所没有考虑到的,反思了一下,主要原因还是因为我平时接触的都是小数据,测试的时候也不会用很多组数据进行测试。所以设计出来的程序和实际使用场景还是有较大的出入。这次是因为使用了不恰当的递归,我正好多测试了几组数据,“成功”将栈空间给挤爆了,将我的问题反映出来了,( ̄ ‘i  ̄;);我将其修改为尾递归实现,还是会爆栈且有很多细小问题,我将其留到非递归实现解决。


一、实现思路

我制作的流程图:
在这里插入图片描述

我的思路历程

在我重新整理的时候,我发现了我之前未考虑到的一些问题,与思维误区。
首先第一个就是我对递归实现的过程没有理顺。我之前先判断这个结点的关键字是否等于T-1,那么那样实现就有很大问题,当当前结点为根结点时,因为根结点允许关键字小于T-1,这种边界情况没有考虑到,解决方法其实十分简单,我们只要保证每次降落到的结点关键字至少为T就可以了,这样就无需考虑根结点的情况,且后续操作也更少。
其次就是,删除操作中很多代码重复使用,导致函数过长,功能划分不明确给后续的梳理,重构带来麻烦,解决方法就是将其中的不同功能划分为一个个函数,这也有利于后续修改。
最后就是本次最大收获( ̄m ̄),不恰当的递归函数会导致栈溢出,不恰当的递归函数会导致栈溢出,不恰当的递归函数会导致栈溢出!!! mmp,刚开始设计的时候没有想到,快完成了才发现。

我的算法描述

首先先判断待删除关键字在不在当前结点。

若待删除关键字不在当前结点:
则需保证降落到一个至少有T个关键字的结点 (这样考虑的好处一是不需要管根结点,二是加强条件使得删除时不需要再进行回溯)
若待降落结点有至少T个关键字则在其进行递归删除;
若待降落结点没有T个关键字,这时需要判断它的相邻兄弟结点有没有一个至少有T个关键字;若有则将x的一个关键字降至待降落结点,再将其至少有T个关键字的兄弟结点的一个关键字升至x,将其相应的孩子指针移至待降落结点。若没有(其相邻兄弟结点都是T-1个关键字)则将其与一个兄弟结点合并,将x的一个关键字移至新合并结点成为其中间关键字,释放被合并的结点。这时候需要考虑x中还有没有关键字,若没有则说明x是根结点,释放x使新合并的结点成为新根结点,否则x中至少有T-1个关键字(这是由每次递归降落至一个至少有T个关键字的结点决定的),然后在待降落结点进行递归删除。

若待删除关键字在当前结点:
这时候就要先判断当前结点是不是叶结点,因为B树的删除只能发生在叶结点;
若当前结点是叶结点,则直接将待删除的关键字的后面的关键字全部进一位即可,因为叶结点无儿子,所以不需要调整儿子数组。
若当前结点不是叶结点,则需判断待删除关键字的相邻兄弟结点是否至少有一个有至少T个关键字,若有则在其相应的兄弟结点中找到相应的关键字代替待删除关键字,然后递归删除替代的关键字;若没有则说明其相邻兄弟都只有T-1个关键字,
这时候合并其相邻兄弟结点并将使待删除关键字成为其中间关键字,在新合并结点中递归地删除待删除关键字。

最后,经过以上递归过程最终实现在叶子结点删除待删除关键字(或其被替代的关键字)。

二、函数解析

1、功能需求分析

  1. 当我删除一个关键字(删除发生在叶结点,无需调整儿子数组),要实现这个目的只需使待删除关键字后面的关键字全部进一位。同样的,在我合并结点时即需要删除父结点中的某个关键字(成为新合并结点的中间关键字)还要删除那个关键字后面的儿子指针,使其指针数组后面的指针全部进一位即可。
    所以,我需要一个函数实现——将关键字数组中的某个关键字后的关键字全部进一位,与儿子数组中位于这个关键字后面的儿子的后面的儿子全部进一位。
  2. 当关键字在为内部结点的当前结点中时,若关键字前后有至少有T个关键字的结点时需要进行关键字代替,再在其相邻结点中找出关键字来代替待删除关键字,由于我想使待删除函数更简洁,所以将判断放在功能函数中。
    所以,我需要一个函数实现——若其相邻结点有至少T个关键字则找出合适的关键字代替待删除关键字。
  3. 当待删除关键字在为内部结点的当前结点中,且待删除关键字左右结点都只有T-1个关键字时,我需要进行关键字合并。
    所以,我需要一个函数实现——将两个结点合并,释放被合并的结点,使待删除关键字成为新合并结点的中间关键字。
  4. 当待降落的结点,只有T-1个关键字,且其兄弟中有一个至少有T个关键字时,需要进行关键字转移。
    所以,我需要一个函数实现——将当前结点的一个关键字下降到待降落结点,将待降落结点的相邻兄弟结点中有至少有T个关键字的兄弟结点的一个关键字升至当前结点。

2、函数实现

结点声明:

typedef int KeyType;
const int T=3;
typedef struct Btreenode{
	KeyType key[2*T-1]={0};								//关键字数组 
	bool leaf=true;												//默认叶子结点为真
	int n=0;															//结点个数 
	struct Btreenode* son[2*T]={nullptr};	//子结点数组 
	struct Btreenode* father=nullptr;			//父指针 
}Btreenode;
typedef Btreenode* B_tree;

1、结点调整函数:
功能:将关键字数组中的某个关键字后的关键字全部进一位,与儿子数组中位于这个关键字后面的儿子的后面的儿子全部进一位。
实现:函数接收结点地址与关键字位置为参数。每次将关键字数组中这个位置后面的关键字覆盖前一个关键字,又因为儿子比关键字多一,所以是用儿子数组中这个位置的加二的儿子覆盖这个位置加一的儿子,同时为了避免数组越界,最多运行到关键字数组末尾的前一个位置即停止循环,最后将结束位置的关键字置零,后一个位置的儿子置空,再将关键字数量减一即可。
代码:

void AdjustNode(B_tree p,int position){
	for(;position<p->n-1;position++){
		p->key[position]=p->key[position+1];
		p->son[position+1]=p->son[position+2];
	}
	p->key[position]=0;
	p->son[position+1]=nullptr;
	p->n--;
}

2.、关键字代替函数:
功能:若其相邻结点有至少T个关键字则找出合适的关键字代替待删除关键字
实现:接收结点地址与关键字位置为参数,先声明两个指针用来标记这个位置关键字的前一个和后一个儿子,与一个初始化为0的标志变量,其主要目的是与后面的操作联动,在函数结束时返回这个标志变量,若为1则说明替换发生在前一个儿子,若为2则说明替换发生在后一个儿子,其他数字都说明未进行关键字替换,这时候需要合并结点。若前一个儿子非空且有至少T个关键字则用其末尾关键字替换结点中待删除关键字同时将标志变量赋值为1,若后一个儿子非空且至少有T个关键字时用其第一个关键字替换待删除关键字并将标志变量赋值为2,最后返回标志变量。
代码:

int KeyReplace(B_tree p,int position){
	B_tree last_son=p->son[position],next_son=p->son[posiyion+1];
	int flag=0;					//如果结束时返回的flag=0则说明未替换 
	if(last_son&&(last_son->n>T||last_son->n==T)){	//如果前一个儿子至少有T个关键字
		p->key[position]=last_son->key[last_son->n-1];
		flag=1;						//说明用其前一个儿子的关键字进行替换 
	}else if(next_son&&(next_son->n>T||next_son->n==T)){
		p->key[position]=next_son->key[0];
		flag=2;						//说明用其后一个儿子进行替换 
	}
	return flag;
}

3、合并函数:
功能:将两个结点合并,释放被合并的结点,使待删除关键字成为新合并结点的中间关键字,并调整父结点。
实现:因为我想使删除函数更简洁,所以我将不同类型的合并结点的操作放在这个函数中。我们首先要考虑的边界情况有待降落的结点没有左兄弟或右兄弟,其次才是一般情况待降落结点既有左兄弟又有右兄弟。先考虑有没有左兄弟再考虑有没有右兄弟,如果传入的位置等于0则说明待降落结点没有左兄弟将其与右兄弟合并,不等于0则说明其有左兄弟将其与左兄弟合并(这样不需要考虑没有右兄弟的情况)。合并的具体操作是将父结点中夹在两个待合并结点中间的关键字下放成为合并结点的中间关键字,然后将待合并的两个结点中的右边的结点数据剪切进左边的结点,释放右边的结点,最后调整父结点。合并后需要注意的一点就是因为递归下降的加强条件保证了每次降落的结点至少都有T个关键字,但是根结点可以少于T个关键字,所以每次合并操作后需要检查合并结点的父结点有无关键字,若无关键字则说明父结点为根结点,让新合并的结点成为新根结点。

void MergeNode(B_tree father,int position){
	B_tree left_brother=nullptr,right_brother=nullptr;
	if(position>0) left_brother=father->son[position-1];
	if(position<father->n) right_brother=father->son[position+1]; 
	if(left_brother){								//有左兄弟与左兄弟合并
		Cat(left_brother,father->son[position],father->key[position]);
		AdjustNode(father,position);
	}else if(right_brother){
		Cat(right_brother,father->son[position],father->key[position]);
		AdjustNode(father,position);
	}
}

4、关键字转移函数:
功能:将当前结点的一个关键字下降到待降落结点,将待降落结点的相邻兄弟结点中有至少有T个关键字的兄弟结点的一个关键字升至当前结点。
实现:先考虑待降落结点的左兄弟有没有至少T个关键字,若有将夹在它们之间的关键字下放到待降落结点,再使其左兄弟的尾端关键字成为其之间的关键字,并将其左兄弟相应的孩子指针转移给待降落结点;反之考虑其右兄弟有没有至少T个关键字,若有则将它们之间的关键字下放到待降落结点,将其右兄弟的首关键字升至当前结点成为待降落结点与其右兄弟的中间关键字,并将其右兄弟结点的相应的孩子指针转移给待降落结点。

void TransferKey(B_tree father,int position){
	B_tree left_brother=nullptr,right_brother=nullptr;
	if(position>0) left_brother=father->son[position-1];
	if(position<father->n) right_brother=father->son[position+1];
	if(left_brother&&left_brother->n>T-1){				//左兄弟有至少T个关键字 
		Sortkey(father->son[position],father->key[position-1]);
		Sortson(father->son[position],left_brother->son[left_brother->n]);
		father->key[position-1]=left_brother->key[left_brother->n-1];
		left_brother->key[left_brother->n-1]=0;
		left_brother->son[left_brother->n]=nullptr;
	}else if(right_brother&&right_brother->n>T-1){//右兄弟有至少T个关键字 
		Sortkey(father->son[position],father->key[position+1]);
		Sortson(father->son[position],right_brother->son[0]);
		father->key[position+1]=right_brother->key[0];
		AdjustNode(right_brother,0);
	}
}

三、 删除函数整体代码

B_tree Delete_Key(B_tree root,KeyType x){ 
	int position=Find_position(root,x),flag=0;			//flag是标志变量用来 
	B_tree left_brother=nullptr,right_brother=nullptr;
	if(position>0) left_brother=root->son[position-1];
	if(position<root->n) right_brother=root->son[position+1];
	if(root->key[position]==x){											//待删除关键字在当前结点中
		if(root->leaf) AdjustNode(root,position);			//当前结点为叶结点 
		else{																					//当前结点为内部结点
			flag=KeyReplace(root,position);							//若待删除关键字的相邻结点有至少T个关键字就进行关键字替换 
			if(flag==0) flag=MergeNode(root,position);	//合并结点 
			}
	}else{																					//待删除关键字不在当前结点 
		if(root->son[position]->n>T-1) flag=5;				//待降落的结点有至少T个关键字
		else{																					//待降落的结点只有T-1个关键字 
			flag=TransferKey(root,position);						//待降落结点的兄弟结点有至少T个关键字就进行关键字转移 
			if(flag==0){																//说明其兄弟结点都只有T-1个关键字,进行关键字合并 
				flag=MergeNode(root,position);
			}
		}
	}
	if(root->n==0){														//当前结点为根结点合并后无关键字,则合并结点成为新根结点 
		B_tree temp=root;
		root=root->son[0];
		delete(temp);
	}
	if(flag==0) return root;
/*①*/else if(flag==1) return Delete_Key(root->son[position],root->son[position]->key[root->son[position]->n-1]);
/*②*/else if(flag==2) return Delete_Key(root->son[position+1],root->son[position+1]->key[0]);
/*③*/else if(flag==3) return Delete_Key(root->son[position-1],x);
/*④*/else if(flag==4) return Delete_Key(root->son[position+1],x);
/*⑤*/else return Delete_Key(root->son[position],x);
}

总结

我的递归实现B树到这里算是失败了,但是在这个过程中我理顺了B树的思想,我将会在非递归实现中进行完善。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值