后台开发学习笔记(八、B树、B+树)

终于到最后一棵树了,最后一颗树是B树,B树是为磁盘或其他直接存取的辅助设备而设计的一种平衡搜索树,在降低磁盘I/O操作数方面要更好一些,许多数据库系统使用B树或者B树的变种来存储信息。

8.1 磁盘介绍

为什么介绍磁盘,因为这个B树就是为了存储在磁盘中使用的,介绍磁盘可以更好的理解B树。

8.1.1 磁盘构造

计算机中有两种存储介质,一种是主存(main memory)通常由硅存储芯片组成,就是内存;还有基于磁盘的辅存(secondary storage),就是硬盘之类的。内存容量一般都比较小,硬盘大小会比内存的大小多几个数量级,如果我们数据量过大,还有需要断电不丢失,就需要存储在硬盘中。

在这里插入图片描述
在这里插入图片描述
这样就很清楚了,驱动器由一个或多个盘片(platter)组成,它们以一个固定的速度绕着一个共同的主轴旋转。每个盘的表面覆盖着一层可磁化的物质。驱动器通过磁臂末尾的磁头来读/写盘片。磁臂可以将磁头向主轴移近或移远。当一个给定的磁头处于静止时,它下面经过的磁盘表面称为一个磁道。多个盘片增加的仅仅是磁盘的容量,而不影响性能。 (出自《算法导论》)
在这里插入图片描述

  1. 磁道
    什么是磁道呢?每个盘片都在逻辑上有很多的同心圆,最外面的同心圆就是 0 磁道。我们将每个同心圆称作磁道(注意,磁道只是逻辑结构,在盘面上并没有真正的同心圆)。硬盘的磁道密度非常高,通常一面上就有上千个磁道。但是相邻的磁道之间并不是紧挨着的,这是因为磁化单元相隔太近会相互产生影响。

  2. 扇区
    那扇区又是什么呢?扇区其实是很形象的,大家都见过折叠的纸扇吧,纸扇打开后是半圆形或扇形的,不过这个扇形是由每个扇骨组合形成的。在磁盘上每个同心圆是磁道,从圆心向外呈放射状地产生分割线(扇骨),将每个磁道等分为若干弧段,每个弧段就是一个扇区。每个扇区的大小是固定的,为 4K。扇区也是磁盘的最小存储单位。

  3. 柱面
    柱面又是什么呢?如果硬盘是由多个盘片组成的,每个盘面都被划分为数目相等的磁道,那么所有盘片都会从外向内进行磁道编号,最外侧的就是 0 磁道。具有相同编号的磁道会形成一个圆柱,这个圆柱就被称作磁盘的柱面,如图 所示

当磁盘驱动器执行读/写功能时。盘片装在一个主轴上,并绕主轴高速旋转,当磁道在读/写头(又叫磁头) 下通过时,就可以进行数据的读 / 写了。一般磁盘分为固定头盘(磁头固定)和活动头盘。固定头盘的每一个磁道上都有独立的磁头,它是固定不动的,专门负责这一磁道上数据的读/写。

活动头盘 (如上图)的磁头是可移动的。每一个盘面上只有一个磁头(磁头是双向的,因此正反盘面都能读写)。它可以从该面的一个磁道移动到另一个磁道。所有磁头都装在同一个动臂上,因此不同盘面上的所有磁头都是同时移动的(行动整齐划一)。当盘片绕主轴旋转的时候,磁头与旋转的盘片形成一个圆柱体。各个盘面上半径相同的磁道组成了一个圆柱面,我们称为柱面 。因此,柱面的个数也就是盘面上的磁道数
参考博客https://www.cnblogs.com/sunsky303/p/11497448.html
http://c.biancheng.net/view/879.html
https://blog.csdn.net/guozuofeng/article/details/90369471

8.1.2 读写效率

磁盘上数据必须用一个三维地址唯一标示:柱面号(磁道)、盘面号、块号(磁道上的扇区)。

读/写磁盘上某一指定数据需要下面3个步骤:
(1) 首先移动臂根据柱面号使磁头移动到所需要的柱面上,这一过程被称为定位或查找 。
(2) 所有磁头都定位到所有盘面的指定磁道上(磁头都是双向的)。这时根据盘面号来确定指定盘面上的磁道。(不是很清楚)
(3) 盘面确定以后,盘片开始旋转,将指定块号(扇区)的磁道段移动至磁头下。
经过上面三个步骤,指定数据的存储位置就被找到。这时就可以开始读/写操作了。

访问某一具体信息,由3部分时间组成:
● 查找时间(seek time) Ts: 完成上述步骤(1)所需要的时间。这部分时间代价最高,最大可达到0.1s左右。
● 等待时间(latency time) Tl: 完成上述步骤(3)所需要的时间。由于盘片绕主轴旋转速度很快,一般为7200转/分(电脑硬盘的性能指标之一, 家用的普通硬盘的转速一般有5400rpm(笔记本)、7200rpm几种)。因此一般旋转一圈大约0.0083s。
● 传输时间(transmission time) Tt: 数据通过系统总线传送到内存的时间,一般传输一个字节(byte)大概0.02us=2*10^(-8)s

为了提高效率,文件系统中有做了数据缓存,等到一定数据要修改的时候,就可以一次性写入到磁盘中,所以我们尽量的减少磁盘存取的次数。

磁盘读取数据是以盘块(block)为基本单位的。位于同一盘块中的所有数据都能被一次性全部读取出来。而磁盘IO代价主要花费在查找时间Ts上。因此我们应该尽量将相关信息存放在同一盘块,同一磁道中。或者至少放在同一柱面或相邻柱面上,以求在读/写信息时尽量减少磁头来回移动的次数,避免过多的查找时间Ts。

所以,在大规模数据存储方面,大量数据存储在外存磁盘中,而在外存磁盘中读取/写入块(block)中某数据时,首先需要定位到磁盘中的某块,如何有效地查找磁盘中的数据,需要一种合理高效的外存数据结构,就是下面所要重点阐述的B-tree结构。

8.2 B树的定义

8.2.1 B树的定义

一颗B树T具有以下性质的有根树(根为T.root)

  1. 每个结点x有下面属性:
    a,x.n,当前存储在结点x中的关键字个数。
    b,x,n个关键字本身x.key1,x.key2,…。x.keyx.n,以非降序存放,使得x.key1≤x.key2≤…≤x.keyx.n
    c,x.leaf,一个布尔值,如果x是叶结点,则为TRUE;如果x为内部结点,则为FALSE。

  2. 每个内部结点x还包含x.n+1个指向其孩子的指针x.c1,x.c2,…,x.cx.n+1。叶结点没有孩子,所以它们的ci属性没有定义。

  3. 关键字x.keyi对存储在各子树中的关键字范围加以分割:如果ki为任意一个存储在以x.ci为根的子树中的关键字,那么
    ki≤x.key1≤k2≤x.key2≤…≤x.keyx.n≤kx.n+1

  4. 每个叶子结点具有相同深度,即树的高度h.

  5. 每个结点所包含的关键字个数有上限和下限。用一个被称为B树的最小度数的固定整数t≥2来表示这些界。
    a,除了根结点以外的每个结点必须至少有t-1个关键字。因此,除了根结点以外的每个内部结点至少有t个孩子。如果树非空,根结点至少有一个关键字。
    b,每个结点至多可包含2t-1个关键字。因此,一个内部结点至多可有2t个孩子。当一个结点恰好有2t-1个关键字时,称该节点是满的。 (来自《算法导论》)

8.2.2 自己总结

算法导论写的真复杂,本来不想写的,但是还是需要一个专业的定义,然后我自己再来一个定义,上面的定义可以简单的理解为,一个结点x有n个关键字,关键字是按非降序存放的,并且在x结点中,还有n+1的指向孩子的结点,各个孩子结点也是按照非降序排列的,x结点可以是内部结点也可以是叶子结点,每个叶子节点都具有相同的深度。
下面是b树的度比较重要,除了根结点以外,每个结点至少有t-1个关键字,并且至多有2t-1个关键字,如果达到2t-1个关键字就需要分裂。

讲了这么多文字,来个B树的图更直观:
在这里插入图片描述

8.2.3 B树的结构

typedef int Elemtype;
	
			
#define BTREE_ENTRY(name, type)			\
	struct name 						\
	{									\
		struct type 	**child;		\
		Elemtype 		*key; 			\
		int 			leaf;			\
		int 			num;			\
	}	
			
typedef struct bTree_node
{
	Elemtype data;							//结点数据
	BTREE_ENTRY(, bTree_node) bst;			//B树结点信息
}_bTree_node;
		
		
typedef struct bTree
{
	struct bTree_node *root;				//指向根结点
	int  degree;							//B树的度数
}_bTree;

从B树的结点也看出是按B树的定义实现的,num为这个结点的关键字个数,leaf标记这个结点是否叶子结点,ley就是这个结点的关键字,child二重指针就是指向孩子的指针。B树的根结点,有一个指向B树的根结点的指针,还有一个degree表示B的度,这个度决定着B树的结点的关键字个数为degree-1≤num≤2*degree-1。

8.3 B树的其他函数

8.3.1 B树搜索

B树的搜索根红黑树也差不多的,不过差别是红黑树是二叉,B树是多叉,这个多叉的选择是根据关键字的大小去寻找各自的子树,递归查询;因为B树有多个结点,一个结点有n个子结点,所以返回值需要返回结点

typedef struct bTree_position
{
	struct bTree_node *x;				//b的结点
	int  	i;							//结点中的第几个元素
}_bTree_position;

/**
    * @brief  B树的搜索
    * @param   p 输出参数
    * @retval 
    */ 
    int bTree_search(struct bTree_node *node, Elemtype k, struct bTree_position *p)
	{
		int i = 0;
		assert(p);
	
		//循环判断k在结点上是哪个位置
		while(i < node->bst.num && k > node->bst.key[i]) {
			i++;
		}

		if(i < node->bst.num && k == node->bst.key[i]) {
			//返回结点的指针和结点的第几个元素
			p->x = node;
			p->i = i;
		} else if(node->bst.leaf)  {		//如果是叶子结点
			p->x = NULL;
			p->i = 0;
		} else {
			bTree_search(node->bst.child[i], k, p);
		}

		return 0;
	}

① 感觉还是先写插入的比较好,不过都写搜索了,就写把,因为这是B树,一个结点有几个关键字,所以返回值组织成一个结构体,这个结果体里面有x结点的指针和x的关键字的下标,暂且这样写吧。
② 搜索的时候,传入结点node,然后遍历这个结点的关键字,如果k大于关键字,关键字需要往后走,如果不大,就是我们感兴趣部分。
③ 首先判断是否等于关键字,如果等于关键字的话,就是找到的这个值,返回。
④ 如果没找到,也分为两种情况,一种是当前结点是叶子结点,这种情况就是说明没有找到关键字,所以返回空。
⑤ 另一种情况是内部结点,内部结点的意思就是还有孩子结点,所以需要递归调用,往孩子结点继续寻找。

8.3.2 B树遍历

B树的遍历比较简单,就是递归加循环,结点到结点之间利用递归,一个结点利用循环,因为一个结点有几个关键字,所以需要循环遍历。
代码:

/**
		* @brief  B树遍历,递归遍历
		* @param	
		* @retval 
		*/ 
		static void btree_printf(struct bTree_node *node)
		{	
			int i = 0;
			if(node == NULL)
				return ;

			//遍历当前结点
		printf("keynum = %d is_leaf = %d\n", node->bst.num, node->bst.leaf);

			for(i=0; i<node->bst.num; i++) {
				printf("%c ", node->bst.key[i]);
			}

			printf("\n");
			for(i=0; i<=node->bst.num; i++) {
				btree_printf(node->bst.child[i]);
			}
			
		}

8.4 B树的插入

8.4.1 创建结点

按照惯例,插入的时候都会先创建结点,这样才符合我们的步骤

/**
    * @brief  B树创建结点
    * @param   p 输出参数
    * @retval 
    */ 
    struct bTree_node *bTree_creat_node(struct bTree *T, Elemtype k, int leaf)
	{
		//申请一个结点
		struct bTree_node *node = (struct bTree_node *)malloc(sizeof(struct bTree_node));
		assert(node);

		//填充数据
		node->data = 0;				//不知道data是怎么用,以后分析一些具体使用B树的实例应该就清楚了
		node->bst.num = 0;			//node结点个数
		node->bst.leaf = leaf;		//是否是叶子结点,1为叶子节点,0为非叶子节点
		node->bst.key = (Elemtype *)calloc(1, (2*T->degree-1)*sizeof(Elemtype));   //2t-1个关键字
		node->bst.child = (struct bTree_node **)calloc(1, (2*T->degree)*sizeof(struct bTree_node *));

		return node;
    }

B树的结点比较复杂,因为要申请2degree-1的关键字和2degree个指向孩子的指针

8.4.2 插入预热

插入结点,还是按原来的步骤一个一个添加,这次我们添加26个英文字母,这个B树我选度为3,关键字个数为2 * 3 - 1=5,孩子结点为 2 * 3 = 6.

  1. 添加A,B,C,D, E
    在这里插入图片描述
    这个关键字个数最大是5,所以前面5个都是直接插入的

  2. 插入F
    插入F的时候,判断到根结点为5的时候,开始分裂
    在这里插入图片描述
    分裂的步骤:先申请一个结点作为头结点,这个结点为s,原来的根结点作为y,从第一个结点开始分裂,就形成上图所示的分裂。
    然后继续插入F,
    在这里插入图片描述
    这样就符合要求,记住是先分裂再添加

  3. 插入G,H
    在这里插入图片描述

  4. 插入I
    遍历到右边结点的时候,发现是满结点,分裂,这个分裂跟结点分裂不一样,这个直接调用分裂函数即可
    在这里插入图片描述
    分裂的结点是右边结点的第3个,调用分裂函数,就可以得出如图所示的结果,然后插入I
    在这里插入图片描述

  5. 插入J K
    在这里插入图片描述

  6. 插入L
    这次又到了满结点,又进行分裂
    在这里插入图片描述
    继续插入L
    在这里插入图片描述

  7. 插入M N
    在这里插入图片描述

  8. 插入O
    分裂
    在这里插入图片描述
    插入
    在这里插入图片描述

  9. 插入P Q
    在这里插入图片描述

  10. 插入R

分裂
在这里插入图片描述
插入
在这里插入图片描述

  1. 插入 S
    插入S结点,根结点是满的,所以要分裂,这个很容易不小心出错,我也是出错了,之后再回来改正的
    在这里插入图片描述
    插入S
    在这里插入图片描述

12.插入T
在这里插入图片描述

  1. 插入U V W
    分裂
    在这里插入图片描述
    插入U V W
    在这里插入图片描述

  2. 插入X Y Z
    分裂
    在这里插入图片描述
    插入
    在这里插入图片描述

至此26个字母的B树就插入完成。

8.4.3 插入结点

通过上的插入顺序,应该了解到B树插入的过程了,插入过程是先判断是否为满结点,如果是满的结点,先需要分裂,这个分裂我们下节再细讲,这里就先调用一个空函数,表示已经分裂了,如果不是满结点,就进行插入,但是这个分裂也分为两种情况,一种是根结点,一种是其他结点,写代码的时候需要注意

代码:

/**
	* @brief  B树插入结点
	* @param   输出参数
	* @retval 
	*/ 
	static int btree_insert_nonfull(struct bTree *T, struct bTree_node *node, Elemtype k) 
	{
		int i = node->bst.num - 1;
		//这个才是真正的插入函数

		if(node->bst.leaf)  {		
			//如果是叶子结点,就可以插入了

			//循环比较关键字,看插入哪个位置
			while(i>=0 && k<node->bst.key[i]) {
				node->bst.key[i+1] = node->bst.key[i];  	
				//能到这一步插入了,就都不是满结点,不需要考虑溢出问题
				i--;
			}

			//以i为分界,往后移动,留下i作为新结点的位置
			node->bst.key[i+1] = k;
			node->bst.num++;
		}else { 					//不是叶子结点
			//循环比较关键字,看插入哪个位置
			while(i>=0 && k<node->bst.key[i]) i--;
		
			//判断child[i]指向的子结点是否是满的
			if(node->bst.child[i+1]->bst.num == T->degree*2-1) {
				//分裂
				btree_split_child(T, node, i+1);
				//分裂完成之后,再判断一下新添加到x结点的i+1的值和k比较
				if(k > node->bst.key[i+1]) i++;
			}

			btree_insert_nonfull(T, node->bst.child[i+1], k);
		}

		return 0;
	}

/**
    * @brief  B树插入结点
    * @param   输出参数
    * @retval 
    */ 
    int bTree_insert(struct bTree *T, Elemtype k)
	{
		//插入结点的时候,要先判断是否是满结点,判断满结点也是分两种情况,一种是根结点,一种是其他结点
		
		//根结点为满结点的时候
		if(T->root->bst.num == T->degree*2-1) {
			//分裂根结点

			//创建结点x
			struct bTree_node *x = bTree_creat_node(T, 0, 0);	
			x->bst.child[0] = T->root;
			T->root = x;
			 
			btree_split_child(T, x, 0);

			int i = 0;
			if(k > x->bst.key[0]) i++;
			btree_insert_nonfull(T, x->bst.child[i], k);
		} else {
			btree_insert_nonfull(T, T->root, k); 
		}

		return 0;
	}

这个插入结点不是完整的代码,还有分裂需要补上,下节补。

8.4.4 分裂结点

B树中插入一个关键字要比二叉搜索树中插入一个关键字复杂的多,不能想二叉搜索树那样,寻找到要插入的位置的时候,直接创建一个新的加点,然后插入;B树的插入,是将一个新的关键字插入到一个已经存在的叶子结点上。由于不能插入到一个满的叶子结点,所以引入了一个操作,叫分裂;分裂是指将一个满的结点y(2degree-1个关键字)按其中间关键字 y.keyi分裂为两个各包含degree-1个关键字的结点,中间关键字被提升到y的父节点,以标识两颗新树的划分点。但是如果y的父结点也是满的,就必须在插入新的关键字之前就进行分裂。

  1. 分裂其他结点
    其他结点的分裂简单一点,没有那么难,需要知道分裂结点的父节点x,和要分裂结点指向的x的下标i,通过x的下标i获取到要分裂的结点y, 然后申请一个新的结点z,然后把y中的一部分数据拷贝到z中,把y中的中间关键字提升到父节点x中,
/**
	* @brief  B树分裂结点
	* @param   要分裂的结点的父节点x,和指向要分裂结点的x的下标i
	* @retval 
	*/ 
	static int btree_split_child(struct bTree *T, struct bTree_node *x, int i)
    {
		int j = 0;
		//获取到要分裂的结点的指针
		struct bTree_node *y = x->bst.child[i];

		//创建一个新的结点
		struct bTree_node *z = bTree_creat_node(T, 0, y->bst.leaf);		

		//拷贝y的一半关键字给z
		for(j = 0; j<T->degree-1; j++)
		{
			z->bst.key[j] = y->bst.key[T->degree+j];   
		}

		//判断是否是是叶子结点,如果不是,拷贝指针
		if(!y->bst.leaf)
		{
			for(j = 0; j<=T->degree-1; j++)
			{
				z->bst.child[j] = y->bst.child[T->degree+j];   
			}
		}

		//更新y,z的num
		y->bst.num = T->degree-1;
		z->bst.num = T->degree-1;

		//移动x的结点,留下i的空位,然后插入
		for(j=x->bst.num; j>=i; j--)
		{
			x->bst.key[j+1] = x->bst.key[j];
		}

		x->bst.key[i] = y->bst.key[T->degree-1];

		//移动x的孩子结点,添加指针z的指针
		for(j=x->bst.num-1; j>=i; j--)
		{
			x->bst.child[j+1] = x->bst.child[j];
		}

		x->bst.child[i+1] = z;
		x->bst.num += 1;

		return 0;
	}

其他结点分裂,只要按照参数传参即可,这个函数内部已经实现了

  1. 分裂根结点
    分裂根结点不一样的地方是要重新申请一个根结点做为x,原来的根结点作为y,x分裂的位置为1,一些细节还是需要注意

代码如下:

//分裂根结点

//创建结点x
struct bTree_node *x = bTree_creat_node(T, 0, 0);	
x->bst.child[0] = T->root;
T->root = x;
 
btree_split_child(T, x, 0);

int i = 0;
if(k > x->bst.key[0]) i++;
btree_insert_nonfull(T, x->bst.child[i], k);

分裂根结点上面也有了,这里在补补。

8.5 B树的删除

树的删除都是比较麻烦的,因为删除树的结点的时候,需要考虑到全面,不像插入的时候,只考虑当前,但是有插入就有删除,这是必须的,所以删除的操作也必须要熟悉。

8.5.1 释放结点

还是先从简答的说起,有没有发现,在其他树删除的时候,都没有这个释放结点,那是因为其他树的时候结点都这么复杂,只要free一次就足够了,但是B树是多子树,每一个结点中升申请了一块内存存放关键字,还有申请了一部分空间存放孩子结点的指针,这样都释放结点的时候都应该得到释放。

代码:

/**
	* @brief  B树释放结点
	* @param   p 输出参数
	* @retval 
	*/ 
	int bTree_destroy_node(struct bTree_node *node)
	{
		assert(node);

		//释放关键字内存
		free(node->bst.key);

		//释放指向孩子结点指针内存
		free(node->bst.child);

		//释放结点
		free(node);

		return 0;
	}

很简单,依次释放内存

8.5.2 删除预热

删除的时候,还是按照老办法,一个一个删除,感受一下删除的情况。

  1. 删除z
    在这里插入图片描述

删除z是最简单的,也符合删除的第一种情况:要删除的元素在叶子节点,所以只需要直接删除。这时候就有人眼尖看到了如果删除A的话呢,A也是叶子节点,是不是直接删除,这个删除A的等下就讲,先简单然复杂。
在这里插入图片描述
2. 删除U
删除U是符合第2种情况,但是第二种情况也分为了3种小情况,删除U是符合2.2的情况,因为后于U的孩子结点的关键字大于T->degree-1,所以按2.2处理。
在这里插入图片描述
3. 删除O
删除O符合2.3这种情况,左右两边的孩子的关键字都等于T->degee-1,所以需要用2.3的归并再删除的方式删除。
归并结果:
在这里插入图片描述
然后删除O:
在这里插入图片描述

  1. 删除A
    这第3种情况算法导论说的我也不是很明白,不过通过程序倒推回来,还是可以理解的,如果理解不对的地方可以在评论中指出,我好改正。
    我的理解是这样的,删除z的情况符合第1个条件,是叶子结点,并且关键字大于T->degree-1,但是如果要删除A呢,这个也是叶子结点,明显跟算法导论说的不太一样,所以我在条件1加了一个条件,以便区别。
    就以删除A举例,A的结点和右结点都只有T->degree-1个关键字,所以不能直接删除,这时候就要利用在递归查找过程中,先判断孩子结点的情况,先判断孩子结点的关键字是否等于T->degree-1,如果不是,就获得这个孩子结点的指针,然后递归调用。
    如果是的话,就需要分类讨论:

  2. 如果这个孩子结点的兄弟结点的关键字大于T->degree个关键字,则将x中的某一个关键字将至孩子结点中,然后再从相邻的结点中提取一个关键字,升到父节点中,这样孩子结点的关键字就等于T->degree个了,可以继续递归调用

  3. 如果这个孩子结点以及所有相邻的兄弟结点都只包含T->degree-1个关键字,则将这个孩子结点和兄弟结点进行合并,然后在把父节点中的一个关键字移到新合并的结点中,这个新的关键字就是新结点的中间关键字。

写了这么多,不知道有没有了解,不了解的研究下代码,就会理解了,代码的逻辑很清晰,不像描述成文字这么难。

删除A步骤:
刚开始递归,判断I的左孩子是等于T->degree-1个关键字,符合3.1情况,开始移位,移位后结果:
在这里插入图片描述
进入CFI结点,继续处理,这次左边孩子结点不存在,右边的孩子结点(DE)满足等于T->degree-1个关键字,所以符合3.2情况
首先合并:
在这里插入图片描述
然后删除:
在这里插入图片描述
删除A完成。

8.5.3 删除遇到的情况

  1. 如果关键字k在结点x中,并且x是叶结点,x的结点的关键字大于T->degree则从x中删除k。

  2. 如果关键字k在结点x中,并且x是内部结点,则要判断如下
    2.1 x结点中前于k的子结点y,至少包含T->degree个关键字,则找出k在以y为根的子树中的前驱k’,递归的删除k’,并在x中用k’代替k。
    2.2 对称地,如果y有小于T->degree个关键字,则检查结点x中后于k的子结点z。如果z至少有T->degree个关键字,则找出k在以z为根的子树中后继k’,递归的删除k’,并在x中用k’替换k。
    2.3 否则,如果y和z都只含有T->degree个关键字,则将k和z全部合并进y中,这样x就失去了k和指向z的指针,并且y现在包含2T->degree-1个关键字,然后释放z,并递归的地从y中删除k。

3.孩子结点关键字等于T->degree-1的时候
3.1 如果这个孩子结点的兄弟结点的关键字大于T->degree个关键字,则将x中的某一个关键字将至孩子结点中,然后再从相邻的结点中提取一个关键字,升到父节点中,这样孩子结点的关键字就等于T->degree个了,可以继续递归调用。
3.2 如果这个孩子结点以及所有相邻的兄弟结点都只包含T->degree-1个关键字,则将这个孩子结点和兄弟结点进行合并,然后在把父节点中的一个关键字移到新合并的结点中,这个新的关键字就是新结点的中间关键字。

8.5.4 合并结点

B树的删除,有点借位的思想,如果不够就往其他孩子结点借,如果左右孩子都不够,就把左右孩子合并成一起,再删除,这次说的就是合并。
代码:

/**
	* @brief  归并结点
	* @param   
	* @retval 
	*/ 
	static int btree_merge(struct bTree *T, struct bTree_node *node, int idx)
    {
		//归并的只要思路,就是把node->bst.child[idx] 和 node->bst.child[idx+1]归并  ,然后插入node->key[idx]作为中间关键字

		int i=0;
		struct bTree_node *left = node->bst.child[idx];
		struct bTree_node *right = node->bst.child[idx+1];

		//data merge
		left->bst.key[T->degree-1] = node->bst.key[idx];			//node->key[idx]作为中间关键字
		for(i=0; i<T->degree-1; i++)
		{
			left->bst.key[T->degree+i] = right->bst.key[i];
		}
		if(!left->bst.leaf)
		{
			for(i=0; i<T->degree; i++)
			{
				left->bst.child[T->degree+i] = right->bst.child[i];
			}
		}
	
		left->bst.num += T->degree;    //还有一个key

		bTree_destroy_node(right);

		//node 删除node[idx],从后往前移 
		for(i=idx+1; i<node->bst.num; i++)
		{
			//这个拷贝的时候需要很注意
			node->bst.key[i-1] = node->bst.key[i];
			node->bst.child[i] = node->bst.child[i+1];
		}

		node->bst.child[node->bst.num] = NULL;
		node->bst.num -= 1;

		if(node->bst.num == 0) {
			T->root = left;
			bTree_destroy_node(node);
		}
	
		return 0;
	}

8.5.5 结点删除

结点删除就是按照8.5.3中那几个情况进行处理的,代码判断逻辑比较清晰明了,好好看看就明白了。
代码:

/**
	* @brief  B树删除结点,递归调用
	* @param   输出参数
	* @retval 
	*/ 
	static void btree_delete_key(struct bTree *T, struct bTree_node *node, Elemtype k) 
	{
		int idx = 0, i;

		if(node == NULL) return ;
		
		//遍历查找k是否在当前结点node中,这个是从0下标找起
		while(idx<node->bst.num && k > node->bst.key[idx]) {
			idx++;
		}
		printf("btree_delete_key %d %d %c %c\n", node->bst.num, node->bst.leaf, k, node->bst.key[idx]);
		if(idx < node->bst.num && k == node->bst.key[idx]) {
			//如果在当前结点上
			
			if(node->bst.leaf) {
				//判断是否是叶子结点,如果是叶子节点就直接删除
				//符合第1种情况
				printf("叶子点\n");
				//删除结点,把后面的结点往前移,这个是叶子结点,不用移动指针
				for(i=idx; i<node->bst.num-1; i++) {
					node->bst.key[i] = node->bst.key[i+1];
				}

				node->bst.key[node->bst.num-1] = 0;
				node->bst.num -= 1;
				
				if(node->bst.num == 0) {	//如果num=0,说明只剩根结点了
					bTree_destroy_node(node);				//释放根结点
					T->root = NULL;
				}
				
				return ;
			} 
			//后面的都不是叶子结点的
			else if(node->bst.child[idx]->bst.num >= T->degree)   {     //前于k的子结点y,至少包含T->degree各个关键字 
				//找出k在以**y为根的子树中的前驱k',递归的删除k'**,并在x中用k'代替k。
				printf("前驱\n");
				struct bTree_node *left = node->bst.child[idx];

				//用k'替换k
				node->bst.key[idx] = left->bst.key[node->bst.num-1];

				//递归替换
				btree_delete_key(T, left, left->bst.key[node->bst.num-1]); 
			}	
			else if(node->bst.child[idx+1]->bst.num >= T->degree)   {     //后于k的子结点z,至少包含T->degree各个关键字 
				//找出k在以**z为根的子树中的后驱k',递归的删除k'**,并在x中用k'代替k。
				printf("后驱\n");
				struct bTree_node *right = node->bst.child[idx+1];

				//用k'替换k
				node->bst.key[idx] = right->bst.key[0];

				//递归替换
				btree_delete_key(T, right, right->bst.key[0]); 
			}	
			else  { 	//y和z都包含T->degree-1个关键字,需要合并,然后在从y中递归删除k
				printf("其他\n");
				btree_merge(T, node, idx);						//合并
				btree_delete_key(T, node->bst.child[idx], k);	//递归删除
			}
		} else {
			//如果不在当前结点,就往孩子结点找
			struct bTree_node *child = node->bst.child[idx];
			
			if(child == NULL)  return ;

			if(child->bst.num == T->degree - 1) {    //判断孩子结点的个数是不是小于最小值,如果是就要特殊处理
				struct bTree_node *left = NULL;
				struct bTree_node *right = NULL;
				
				if (idx - 1 >= 0)
					left = node->bst.child[idx-1];
				if (idx + 1 <= node->bst.num) 
					right = node->bst.child[idx+1];

				if((left && left->bst.num >= T->degree) ||
					(right && right->bst.num >= T->degree))  {
						
						int richR = 0;
						if(right) richR = 1;
						if (left && right) richR = (right->bst.num > left->bst.num) ? 1 : 0;
						printf("右孩子\n");
						if(right && right->bst.num >= T->degree && richR) {   //找到右边兄弟替补
							//先把node结点的数据往child里放,放到child最后一个key中
							child->bst.key[child->bst.num] = node->bst.key[idx];
							//把right的第一个孩子指针挂接到child的最后一个指针上
							child->bst.child[child->bst.num+1] = right->bst.child[0];
							child->bst.num++;

							//把右孩子结点往父节点提
							node->bst.key[idx] = right->bst.key[0];

							//右孩子结点往前移
							for(i=0; i<right->bst.num-1; i++) {
								right->bst.key[i] = right->bst.key[i+1];
								right->bst.child[i] = right->bst.child[i+1];
							}

							right->bst.child[right->bst.num-1] = right->bst.child[right->bst.num];
							right->bst.child[right->bst.num] = NULL;
							right->bst.num--;
						} else{   //找到左边兄弟替补
							//左边跟右边不是对称关系,操作有点不一样,不过大体都一样的
							printf("左孩子\n");
							//先移动child的结点,空出第一个结点,等到父节点node插入元素
							for(i=child->bst.num; i>0; i--) {
								child->bst.key[i] = child->bst.key[i-1];
								child->bst.child[i+1] = child->bst.child[i];
							}

							child->bst.child[1] = child->bst.child[0];	
							//把左边孩子最后一个孩子指针挂接到child的0
							child->bst.child[0] = left->bst.child[left->bst.num];
							//把父结点的关键字赋值给left的0下标
							child->bst.key[0] = node->bst.key[idx-1];
							child->bst.num++;

							//把左孩子的最后一个元素赋值给父节点的元素
							node->bst.key[idx-1] = left->bst.key[left->bst.num];
							left->bst.child[left->bst.num] = NULL;
							left->bst.num--;
						}
				} else if((!left || left->bst.num == T->degree-1) && //左右兄弟都小于T->degree-1的时候
					(!right || right->bst.num == T->degree-1)) {

					if(left && left->bst.num == T->degree-1) {   //左边兄弟存在,并且满足T->degree-1
						btree_merge(T, node, idx-1);	
						child = left;
					} else if(right && right->bst.num == T->degree-1) {  //右边兄弟存在,并且满足T->degree-1
						btree_merge(T, node, idx);
						btree_printf(T->root);
						printf("\n");
					}
				}
			}

			//如果不是,直接调用删除函数
			btree_delete_key(T, child, k);
		}		
	}

这次B树写的跟实际运用,还是很大差别的,以后有时间研究一下实际工程中的应用,再写一篇真正实用的,这次就相当预习了。

发布了32 篇原创文章 · 获赞 26 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/C1033177205/article/details/103753439