详细图解高阶搜索树B树及C语言实现

目录

一、什么是B-tree

二、查找

三、插入

四、删除


前言
对于大量的数据而言,链表的线性访问时间太慢,不宜使用。本章节将会介绍一种简单的数据结构:树(tree),其大部分操作的运行时间平均为O(logN)。在数据结构中树是非常有用的抽象概念,在本篇中我们将讨论一棵高阶搜索树——b树。前排提示:高阶搜索树的代码难度高出一个量级,建议先把逻辑理顺后再去看代码哦

一、什么是B-tree

虽然迄今为止我们所看到的查找树都是二叉树,但是还有一种常用的查找树不是二叉结构的,这种树叫做B树(B-tree)。

那么问题来了,既然已经有了诸如AVL树的查找树,为什么还要存在B树呢?因为平衡树的数据放在内存当中,但在实际的项目中,数据量特别的大,要放在磁盘中,而面临大量数据时二叉树的高度大意味着与磁盘交互次数多,而每一次磁盘的I/O都是机械运动,读取速度相对于内存来说是很慢的,为了降低树的高度、减少磁盘I/O,B树就此诞生。

B树具有下列特性:

  • 是一棵多路平衡树,一个节点有多个数据,通常要求空间大小和磁盘块的大小一致;
  • 在逻辑上,所有的叶子都在同一层且不带信息;
  • 对于节点规定了上界和下界,最常见的每个节点(根节点除外)包含【t-1,2t-1】个关键字(其中t是最小的度,t>=2),不是硬性规定,根据实际中的磁盘块来决定;
  • 如果一个节点有x个关键字,就有x+1个孩子;
  • 每一个节点的关键字按照升序排序,x和y之间孩子的范围就是(x, y)。

为了使大家在概念上更方便理解,正如先前在二叉排序树中做的那样,我们使实际的数据存储在叶子上,也可以存储在内部节点中。实际上B树有多种定义,这些定义在一些次要的细节上不同于我们定义的结构。如下所示就是一棵B树:

这里还要提一个概念,图中的B树是4阶B树的一个例子,它的更流行的称呼是2-3-4树,而3阶B树称作2-3树;我们将通过这棵2-3-4树来描述B树增删查改。

二、查找

B树的查找和二叉排序树的查找过程十分相似,唯一不同的是:二叉排序树的任意一个节点最多只能有两个孩子,而B树则最多可以有M+1个孩子(M为节点中关键字个数),但这并不影响我们用与二叉排序树相同的查找方法,只需要多一步在节点中的遍历(可以是顺序也可以是二分,方便就行),我在代码中对返回的结果做了封装,这样过程会更加规范,当然效果是一样的。

/*查找返回节点下标的函数 从1到keynum找key*/
int search(BTree p, KeyType key)
{
	int i = 1;
	while (i <= p->keynum && key > p->key[i])
	{
		i++;
	}
	return i;
}

/*返回封装的结果集的查找函数 不是真正进行查找步骤*/
void searchNode(BTree tree, KeyType key, Result& r)
{
	int i;
	int found = 0;//标记查找成功或者失败 成功是1 失败是0
	//先定义指针指向根节点
	BTree p = tree;
	BTree q = NULL;//有时候需要指向双亲节点的指针
	while (p != NULL)
	{
		i = search(p, key);
		if (i <= p->keynum && key == p->key[i])
		{
			//不允许重复所以找到就是查找失败 说明这个数据已经存进去了
			found = 1;
		}
		else
		{
			q = p;
			p = p->ptr[i - 1];//指针下移
		}
	}
	//如果查找到了
	if (found == 1)
	{
		r.pt = p;
		r.i = i;
		r.tag = found;
	}
	//没有查找到
	else
	{
		//没有找到就把父节点传回去 利于后续操作
		r.pt = q;
		r.i = i;
		r.tag = found;
	}
}

三、插入

B树的插入规则与二叉排序树的插入类似,即一个节点中的关键字总是有序的,并且总是要插入到实际的叶子节点;不同的是,为了满足B树的高度可控这一特性,需要时刻判断是否超过了规定的存储最大容量,如果达到容量上限就要进行分裂(split);

有如上一棵2-3树,当我们试图插入新关键字K时,发现K所属的节点已经满了,插入K将使得这个节点拥有四个关键字,这是不允许的,这时我们就要分裂,从中间对半分,得到两个新的节点;

但此时我们又发现,分裂出的新节点的父节点现在有了四个孩子,而它只有两个关键字,最多只能有三个孩子,这样又带来了一个新的问题,当然解决的办法也很简单,我们可以把中间值上移,是父节点的关键变成三个,这样父节点最大就能有四个孩子了。

分裂是维持B树性质的重要操作之一,一定要把逻辑理清楚!接下来我们对上文中的2-3-4树再过一遍完整的插入流程:

第一步:判断根节点是否初始化,如果没有,初始化root为根节点;

第二步:当我们插入节点i,与根节点p比较,小于就往左边插入;

第三步:发现p的左孩子节点已经满了,需要分裂;

第四步:继续比较,比L小,插L的左子树;

第五步:判断L的左子树是否满了,满了就分裂,没满就挨个比较找到正确的插入位置插入,结束。

我们会发现在执行分裂操作时,永远都是从中间对半分、把中间值往上递,且这个步骤在插入的过程中会一直检测并进行,不需要像AVL树一般有一个回溯的过程,像这种平衡的机制我们称作自平衡,也可见B树是不存在不平衡的机会的;

而要简单的实现自平衡的过程,我们就需要在以往的树的结构的代码上做点修改,定义一个指针指向节点的父亲。

树的结构体定义和一些宏;

#define NOT_INIT 0//没有初始化的标志
#define HAVE_BEEN_INIT 1//初始化了的标志
#define TRUE 1
#define FALSE 0
#define m 3//B树的阶 
typedef struct BTreeNode{
	KeyType key[m + 1];//关键字数组 0号元素不用 m是最大元素个数
	struct BTreeNode* ptr[m + 1];//从0开始指向子树的指针
	struct BTreeNode* parent;//指向父级的指针
	int keynum;//当前节点中关键字的个数 也就是节点的大小
}BTnode, *BTree;//B树节点的类型和根节点(树)的类型

代码实现(建议先把逻辑理顺后再看)

代码实现在逻辑的基础上还需要考虑各种可能的复杂情况,根据上文的举例,这里总结出执行插入的大致框架(画的不是很好,但是大概意思就是这样):

为方便大家理解代码,这里代码的顺序是按照流程图上的顺序来的,实际上应该把顺序反过来或者在前面声明。


/*初始化一棵树*/
void initBTreeOperation(BTree& tree)
{
	if (status == HAVE_BEEN_INIT)
	{
		//已经初始化过了
		printf("已经初始化过了\n");
	}
	else
	{
		tree = NULL;
		status = HAVE_BEEN_INIT;
	}
}

/*在B树中插入一个值 */
void insertKeyOperation(BTree& tree, KeyType key)
{
	Result r;//封装结果集
	if (status == NOT_INIT)
	{
		//树没有初始化
		printf("请先初始化\n");
		return;
	}
	if (tree == NULL)
	{
		//此时树是空树
		printf("树是空树\n");
	}
	//如果是索引 不允许重复的情况
	//查找有没有相同的值
	searchNode(tree, key, r);
	if (r.tag == 1)
	{
		//关键字已经存在
		printf("关键字已经存在\n");
	}
	else
	{
		//执行插入操作
		insertBTree(tree, key, r.pt, r.i);
	}
}

/*
在B树tree中q节点相关的位置 i - 1 ~ keynum 之间插入关键字
*/
void insertBTree(BTree& tree, KeyType key, BTree q, int i)//q和i是封装好的节点和下标
{
	BTree temp;
	int finished = FALSE;//表示循环完结状态的变量
	int isNeed_NewNode = FALSE;//表示是否需要创建一个新的节点
	//此时父节点是空的
	if (q == NULL)
	{
		//代表这个树连根都没有
		//创建一个新的节点
		newRoot(tree, NULL, NULL, key);
	}
	else
	{
		//整体的插入
		temp = NULL;
		//插入从上往下插入 不需要递归 只需要循环 没有回溯
		//finished标志的变化只存在于现有的节点数量里插入 
		//如果需要新的节点插入没有结束 需要判断是否需要创建一个新的节点
		while(finished == FALSE && isNeed_NewNode == FALSE)
		{
			//肯定是先插入 将关键字和temp 插入q的q->key[i]和q->ptr[i]
			insert(q, i, key, temp);
			//插入完以后判断 如果q的关键字个数小于最大个数m 说明树的结构没有改变
			if (q->keynum < m)
			{
				finished = TRUE;
			}
			//否则就要拆分
			else
			{
				//先找中间位置
				int s = (m + 1) / 2;
				split(q, s, temp);
				//还要向父级插入s的关键字
				key = q->key[s];
				if (q->parent != NULL)
				{
					//往双亲的位置插入s关键字
					q = q->parent;
					i = search(q, key);
				}
				else
				{
					//如果双亲节点为空 说明要新建一个节点
					isNeed_NewNode = TRUE;
				}
			}
		}
		//如果需要创建一个新的节点
		if (isNeed_NewNode == TRUE)
		{
			newRoot(tree, q, temp, key);
		}
	}
}

/*
 将关键字和temp 插入q的q->key[i]和q->ptr[i]
 q是插入的目标节点 i是位序 key是关键字 ap是新节点的指针
*/
void insert(BTree& q, int i, KeyType key, BTree ap)
{
	for (int j = q->keynum; j >= i; j--)
	{
		//将数组后移
		q->key[j + 1] = q->key[j];
		q->ptr[j + 1] = q->ptr[j];
	}
	q->key[i] = key;
	q->ptr[i] = ap;
	if (ap != NULL)
	{
		ap->parent = q;
	}
	q->keynum++;
}

/*
分裂步骤
将q节点分裂成两个节点 前一半不动 后一半放进新节点ap中 s是中间位序
*/
void split(BTree& q, int s, BTree ap)
{
	ap = (BTree)malloc(sizeof(BTnode));
	ap->ptr[0] = q->ptr[s];
	for (int i = s + 1, j = 1; i <= q->keynum; i++, j++)
	{
		ap->key[j] = q->key[i];
		ap->ptr[j] = q->ptr[i];
	}
	ap->keynum = q->keynum - s;
	ap->parent = q->parent;
	//ap的孩子的父节点也要更新
	for (int i = 0; i <= ap->keynum; i++)
	{
		if (ap->ptr[i] != NULL)
		{
			ap->ptr[i]->parent = ap;
		}
	}
	q->keynum = s - 1;
}

/*生成新的根节点*/
void newRoot(BTree& tree, BTree p, BTree q, KeyType key)//pq是tree的左右孩子
{
	tree = (BTree)malloc(sizeof(BTnode));
	tree->keynum = 1;
	tree->key[1] = key;
	tree->ptr[0] = p;
	tree->ptr[1] = q;
	//别忘记处理孩子的父级指针
	if (p != NULL)
	{
		p->parent = tree;
	}
	if (q != NULL)
	{
		q->parent = tree;
	}
	tree->parent = NULL;//别忘记根也有父级 只不过是NULL
}

个人为如果把逻辑理顺了,再结合流程图去理解,插入和分裂的代码肯定是没问题了的,那么接下来是删除操作。

四、删除

对于B树的删除我们同样需要分成三种情况:

情况一:如果待删除数据data在节点x中,并且x是叶子节点,直接删除;

情况二:如果待删除的数据data在节点x中,并且是内部的节点,这时候又分为三种情况:

①如果data的前第一个孩子y至少有最小关键字个数+1个数据,找data的直接前驱p,删除data,并用p换掉data;

当我们要删除的数据是G时,发现G所在的节点是内部节点x,如果直接删除G,那么x的关键字个数与其孩子的个数就会矛盾,为了使操作尽可能的简单,我们发现G的左孩子y的关键字个数明显大于最小关键字个数+1个数据,这时用节点y的最后一个关键即G的直接前驱F替换G,既不会影响x节点的平衡,又不会影响y节点的平衡,还保证了关键字的有序性;

②如果y节点已经是最小元素个数了,找data的后第一个孩子z至少最小关键字个数+1个数据,找data的直接后继q,删除data,并用q换掉data;

情况②与情况①的意思是一样的,如果节点x的左孩子关键字个数小于最小关键字个数+1个数据,为了保持x与它孩子的性质不改变,这时判断x的右孩子z的关键字个数是否大于最小关键字个数+1个数据,如果大于就向节点z“借”,同样不会影响x节点的平衡,也不会影响z节点的平衡,还保证了关键字的有序性;

③如果前后都不够,就合并y和z;

假设完成某些操作后的树如上所示,此时再删除关键G,会发现它的左孩子关键字只剩下D,而它的右孩子关键字只剩下了K,左右孩子都不够借的,就把D所在的节点y和K所在的节点z合并,这一步和插入中的分裂操作正好相反,但是意义是一样的;

情况三:修正;

很明显情况一、二只能保证当前节点的平衡不被破坏,但是删除导致的后果可能是多个节点的平衡被破坏,这时候就需要再去对整棵树进行修正。

如果当待删除数据data不再是内部节点,那么它一定在子树c上,且此时c也只有最小关键字个元素时,也分为两种情况:

情况①:和内部节点一样,如果c的兄弟至少有最小关键字个数+1个数据,将x的某一数据下移,c兄弟的某一数据上移到父节点x;

当我们要删除数据B时,发现B所在的节点是实际中的叶子节点c,且删除B之后c的关键字个数已经小于最小关键字个数,这时c的兄弟的关键字个数大于最小关键字个数+1,就将父节点x的对应的关键字下移,将兄弟节点的第一个关键字上移(此时兄弟节点是右兄弟的情况),如此也不会破坏父节点、当前节点和兄弟节点的平衡;

因为是修正操作,所以不光要检测当前节点,还要检测当前节点的父节点、一直到根节点,持续向上地修正,从而保证整棵树的性质;

情况②:如果c和它的兄弟都只包含最少,就合并;

叶子的合并的操作和内部节点的合并是一样的,就不再过多描述了,最后也需要向上修正;

学到这里可能会点懵:什么又是内部节点又是叶子的、什么合并什么旋转啊...不要慌,这说明你在思考了。其实从整体来看B树的删除无非就是内部节点的删除和叶子节点的删除,它们之间的区别就在于当删除节点的兄弟有多的关键字时,旋转(借关键字)的步骤有些差异,而合并操作其实是一样的;并且只要理顺:插入和删除其实都是在实际的叶子中进行的,然后不断向上检测平衡性就行了。

代码实现(建议先把逻辑理顺后再看)

当然在代码中我们需要考虑到的情况要更多更复杂,但是基本的逻辑是没有变的,这里同样大致总结了一下删除的整体流程(仅供参考,以逻辑为准):

为了和流程图对照上,这里还和插入一样更改了代码的顺序:

/*封装删除操作*/
void deleteKeyOperation(BTree& tree, KeyType key)
{
	Result r;
	if (status == NOT_INIT)
	{
		//树没有初始化 请先初始化
		printf("请先初始化\n");
	}
	if (tree == NULL)
	{
		//此时树是空树
		printf("树是空树\n");
	}
	else
	{
		searchNode(tree, key, r);
		if (r.tag == 1)
		{
			//如果是1说明查找到了 可以删除 和 插入正好相反
			//需要执行删除操作
			deleteBTree(tree, r.pt, r.i);

		}
		else
		{
			//关键字不存在
			printf("找不到该关键字\n");
		}
	}
}

/*
删除tree上p节点的第i个关键字
*/
void deleteBTree(BTree& tree, BTree& p, int i)
{
	if (p->ptr[i] != NULL)//不是叶子节点 是内部节点时
	{
		//找到后继的下层内部节点的最小关键字来替换
		successor(p, i);
		//删除最下层的非叶子节点的最小关键字
		deleteBTree(tree, p, 1);
	}
	else
	{
		//从节点p中删除key[i]
		removeNode(p, i);
		//判断是否需要调整Btree 看当前关键字是否小于最少关键字
		if (p->keynum < (m - 1) / 2)//上界和下界都是人为定义的
		{
			restore(tree, p);
		}
	}
}

/*找到后继的下层内部节点的最小关键字来替换*/
void successor(BTree& p, int i)
{
	BTree leaf = p;
	if (p == NULL)
	{
		return;
	}
	leaf = leaf->ptr[i];//指向子树
	while (leaf->ptr[0] != NULL)//只要还有孩子 就继续往下走
	{
		leaf = leaf->ptr[0];
	}
	p->key[i] = leaf->key[1];
	p = leaf;
}

/*移除节点 从后往前覆盖*/
void removeNode(BTree& p, int i)
{
	for (int j = i; j < p->keynum; j++)
	{
		p->key[j] = p->key[j + 1];
		p->ptr[j] = p->ptr[j + 1];
	}
	p->keynum--;
}

/*调整树 p是要调整的节点*/
void restore(BTree& tree, BTree& p)
{
	BTree parent, leftBrother, rightBrother;//p的父节点、和左右兄弟
	parent = p->parent;
	//如果父节点不为空的情况 就是不是根节点的情况
	if (parent != NULL)
	{
		//寻找左右兄弟 向兄弟借值
		//先要找到p节点在父节点的哪一个孩子
		int i;
		for (i = 0; i < parent->keynum; i++)
		{
			if (parent->ptr[i] == p)
			{
				break;
			}
		}
		//如果p是最左边的孩子 就不需要找左边的兄弟了 右边同理
		if (i > 0)
		{
			leftBrother = parent->ptr[i - 1];
		}
		else
		{
			leftBrother = NULL;
		}
		if (i < parent->keynum)
		{
			rightBrother = parent->ptr[i + 1];
		}
		else
		{
			rightBrother = NULL;
		}
		//如果左右兄弟有富余 就借
		if ((leftBrother != NULL && leftBrother->keynum >= (m + 1) / 2) ||
			(rightBrother != NULL && rightBrother->keynum >= (m + 1) / 2))
		{
			borrowFromBro(p, parent, leftBrother, rightBrother, i);
		}
		//如果左右兄弟都没有富余 就合并
		else
		{
			if (leftBrother != NULL)
			{
				//和左兄弟合并
				mergeWithLeftBro(tree, p, leftBrother, parent, i);
			}
			else if (rightBrother != NULL)
			{
				//和右兄弟合并
				mergeWithRightBro(tree, p, rightBrother, parent, i);
			}
			//如果当左右子树不存在时
			else
			{
				//改变根节点
				for (int j = 0; j <= parent->keynum + 1; j++)
				{
					if (parent->ptr[j] != NULL)
					{
						tree = parent->ptr[j];
						break;
					}
					tree->parent = NULL;
				}
			}
		}
	}
	else
	{
		BTree temp;
		//父节点为空的情况 p就是根节点
		//去掉父节点 使树减一层
		for (int j = 0; j <= p->keynum; j++)
		{
			if (p->ptr[j] != NULL)
			{
				temp = p;
				p = p->ptr[j];
				temp->ptr[j] = NULL;
				free(temp);
				break;
			}
		}
		tree = p;
		tree->parent = NULL;
	}
}

/*向左兄弟或者右兄弟去借关键字*/
void borrowFromBro(BTree& p, BTree& parent, BTree& leftBrother, BTree& rightBrother,int i)
{
	//向左兄弟借 左兄弟的值都小于p的值 所以要把p的值右移
	if (leftBrother != NULL && leftBrother->keynum >= (m + 1) / 2)
	{
		for (int j = p->keynum + 1; j > 0; j--)
		{
			if (j > 1)
			{
				p->key[j] = p->key[j - 1];
			}
			p->ptr[j] = p->ptr[j - 1];
		}
		p->ptr[0] = leftBrother->ptr[leftBrother->keynum];
		if (p->ptr[0 != NULL])
		{
			p->ptr[0]->parent = p;
		}
		leftBrother->ptr[leftBrother->keynum] = NULL;
		//被删除节点p要存父节点的关键字
		p->key[1] = parent->key[i];
		//父节点的关键字变成左兄弟的最大
		parent->key[i] = leftBrother->key[leftBrother->keynum];
		//当前节点关键字加一 左兄弟节点关键字减一
		leftBrother->keynum--;
		p->keynum++;
	}
	else if (rightBrother != NULL && rightBrother->keynum >= (m + 1) / 2)
	{
		p->key[p->keynum + 1] = parent->key[i + 1];
		//子树指针指向右兄弟最小的子树的指针
		p->ptr[p->keynum + 1] = rightBrother->ptr[0];
		if (p->ptr[p->keynum + 1] != NULL)
		{
			p->ptr[p->keynum + 1]->parent = p;
		}
		p->keynum++;
		//父节点向右兄弟要关键字
		parent->key[i + 1] = rightBrother->key[1];
		//将右兄弟的关键字向左移
		for (int j = 0; j <= rightBrother->keynum; j++)
		{
			if (j > 0)
			{
				rightBrother->key[j] = rightBrother->key[j + 1];
			}
			rightBrother->ptr[j] = rightBrother->ptr[j + 1];
		}
		rightBrother->ptr[rightBrother->keynum] = NULL;
		rightBrother->keynum--;
	}
}

/*和左兄弟合并*/
void mergeWithLeftBro(BTree& tree, BTree& p, BTree& leftBrother, BTree& parent, int i)
{
	//先把父节点的第i个关键字拿下
	leftBrother->key[leftBrother->keynum + 1] = parent->key[i];
	leftBrother->ptr[leftBrother->keynum + 1] = p->ptr[0];
	if (leftBrother->ptr[leftBrother->keynum + 1] != NULL)
	{
		leftBrother->ptr[leftBrother->keynum + 1]->parent = leftBrother;
	}
	leftBrother->keynum++;
	//再把p中的关键字一个个赋给左兄弟
	for (int j = 1; j <= p->keynum; j++)
	{
		leftBrother->key[leftBrother->keynum + j] = p->key[j];
		leftBrother->ptr[leftBrother->keynum + j] = p->ptr[j];
		if (leftBrother->ptr[leftBrother->keynum + j] != NULL)
		{
			leftBrother->ptr[leftBrother->keynum + j]->parent = leftBrother;
		}
	}
	leftBrother->keynum += p->keynum;
	parent->ptr[i] = NULL;
	free(p);
	//还要对父节点进行左移
	for (int j = i; j <= parent->keynum; j++)
	{
		parent->key[j] = parent->key[j + 1];
		parent->ptr[j] = parent->ptr[j + 1];
	}
	parent->ptr[parent->keynum] = NULL;
	parent->keynum--;
	//还要检查父节点是不是违反了规则
	//如果父节点是根节点
	if (tree == parent)
	{
		//当根节点没有关键字的时候 才进行调整
		if (parent->keynum == 0)
		{
			for (int j = 0; j <= parent->keynum + 1; j++)
			{
				if (parent->ptr[j] != NULL)
				{
					tree = parent->ptr[j];
					break;
				}
				tree->parent = NULL;
			}
		}
	}
	else
	{
		//如果不是根节点 就判断是否需要重新进行调整
		if (parent->keynum < (m - 1) / 2)
		{
			restore(tree, parent);
		}
	}
}

/*和右兄弟合并*/
void mergeWithRightBro(BTree& tree, BTree& p, BTree& rightBrother, BTree& parent, int i)
{
	//先要把右兄弟的关键字右移 为p的关键字和父节点的关键字空位
	for (int j = rightBrother->keynum; j > 0; j--)
	{
		if (j > 0)
		{
			//为p的关键字和父节点的关键字空位
			rightBrother->key[rightBrother->keynum + 1 + p->keynum] = rightBrother->key[j];
		}
		rightBrother->ptr[rightBrother->keynum + 1 + p->keynum] = rightBrother->ptr[j];
	}
	//把父节点的那一个关键字拿下
	rightBrother->key[p->keynum + 1] = parent->key[i + 1];
	//再把p中的关键字一个个赋给右兄弟
	for (int j = 0; j <= p->keynum; j++)
	{
		if (j > 0)
		{
			//为p的关键字和父节点的关键字空位
			rightBrother->key[j] = p->key[j];
		}
		rightBrother->ptr[j] = p->ptr[j];
		if (rightBrother->ptr[j] != NULL)
		{
			rightBrother->ptr[j]->parent = rightBrother;
		}
	}
	rightBrother->keynum += p->keynum + 1;
	parent->ptr[i + 1] = NULL;
	free(p);
	for (int j = i + 1; j <= parent->keynum; j++)
	{
		parent->key[j] = parent->key[j + 1];
		parent->ptr[j] = parent->ptr[j + 1];
	}
	//如果父节点在关键字减少之前只有一个节点 就需要把父节点的右孩子赋值给左孩子
	if (parent->keynum == 1)
	{
		parent->ptr[0] = parent->ptr[1];
	}
	parent->ptr[parent->keynum] = NULL;
	parent->keynum--;
	//还要对父节点进行左移
	for (int j = i; j <= parent->keynum; j++)
	{
		parent->key[j] = parent->key[j + 1];
		parent->ptr[j] = parent->ptr[j + 1];
	}
	parent->ptr[parent->keynum] = NULL;
	parent->keynum--;
	//还要检查父节点是不是违反了规则
	//如果父节点是根节点
	if (tree == parent)
	{
		//当根节点没有关键字的时候 才进行调整
		if (parent->keynum == 0)
		{
			for (int j = 0; j <= parent->keynum + 1; j++)
			{
				if (parent->ptr[j] != NULL)
				{
					tree = parent->ptr[j];
					break;
				}
				tree->parent = NULL;
			}
		}
	}
	else
	{
		//如果不是根节点 就判断是否需要重新进行调整
		if (parent->keynum < (m - 1) / 2)
		{
			restore(tree, parent);
		}
	}
}
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

后记

至此,第一棵高阶搜索树B树就结束了,其实理顺了逻辑会发现并不难,只是需要考虑的情况很多很复杂,但是慢慢啃一定能学会的!因为提到过B树是要与磁盘做交互才能体现出其优越性,而本文的重点在于讲清B树增删查改的逻辑,所以就没有main函数的测试了,作为初学者的我们只需要能够理解理顺它的思想、逻辑和代码就足够了;

我自认为每一句代码都有过详细的注释了,但是一定一定一定要理顺逻辑再去看代码(重要的事情说N遍)如果对于代码还有疑问的欢迎与我联系!

猜你喜欢

转载自blog.csdn.net/ZER00000001/article/details/125969029