B-树的实现(第九章 P239 算法9.13,9.14)

文章论述部分转自:https://blog.csdn.net/qq_35644234/article/details/66969238

 

1、背景知识

 

二叉查找树查询的时间复杂度是 O\left ( log_{2}N \right ),从算法逻辑上来讲,二叉查找树的查找速度和比较次数都是最小的。但是,我们不得不考虑一个现实问题:磁盘 IO 。数据库索引是存储在磁盘上的,当数据量比较大的时候,索引的大小可能有几个 G 甚至更多。当我们利用索引查询的时候,显然不能把整个索引全部加载到内存。能做的只有逐一加载每一个磁盘页,这里的磁盘页对应着索引树的节点。所以最坏的情况下,磁盘 IO 次数等于索引树的高度。

这样导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下。既然如此,为了减少磁盘 IO 次数,就需要把原本 “瘦高” 的树结构变得 “矮胖” 。这就是 B树的特征之一。

 

 

2、B-树的介绍

 

B-树其实就是我们平时所说的B树,除了B-树外,还有另外一种叫B+树,我们这里先介绍什么是B-树: 

B-树是一种平衡的多路查找树,它在文件系统中很有用。B-树的结构有如下的特点: 
一棵度为m的B-树称为m阶B-树。一个结点有k个孩子时,必有k-1个关键字才能将子树中所有关键字划分 为k个子集。B-树中所有结点的孩子结点最大值称为B-树的阶,通常用m表示。从查找效率考虑,一般要求 m≥3。一棵m阶的B-树或者是一棵空树,或者是满足下列要求的m叉树:

  • 树中的每个结点至多有m颗子树。
  • 若根结点不是叶子结点,则至少有两颗子树。
  • 除根结点外,所有非终端结点至少有[ m/2 ] ( 向上取整 )颗子树。
  • 所有的非终端结点中包括如下信息的数据(n,A0,K1,A1,K2,A2,….,Kn,An) ,其中:Ki(i=1,2,…,n)为关键码,且Ki < K(i+1),Ai 为指向子树根结点的指针(i=0,1,…,n),且指针A(i-1) 所指子树中所有结点的关键码均小于Ki (i=1,2,…,n),An 所指子树中所有结点的关键码均大于Kn. n 为关键码的个数。
  • 所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。
     

 

 

3、B-树的基本操作–查找介绍

 

我们先给出如下的一个4阶的B-树结构。 

如上图所示,这是我们的一个4阶的B-树,现在假设我们需要查找45这个数是否在B-树中。

  1. 从根节点出发,发现根节点a有1个关键字为35,其中45>35,往右子树走,进入节点c
  2. 发现结点c有2个关键字,其中其中43<45<78,所以进入结点g。
  3. 发现结点g有3个关键字,其中3<45<47,所以继续往下走,发现进入了结束符结点:F,所以45不在B-树中

OK,我们从上述的查找的过程可以得出,在B-树的查找过程为:

  • 在B- 树中查找结点
  • 在结点中查找关键字。

由于B- 树通常存储在磁盘上, 则前一查找操作是在磁盘上进行的, 而后一查找操作是在内存中进行的, 即 在磁盘上找到指针p 所指结点后, 先将结点中的信息读入内存, 然后再利用顺序查找或折半查找查询等于K 的关键字。显然, 在磁盘上进行一次查找比在内存中进行一次查找的时间消耗多得多. 因此, 在磁盘上进行查找的次数、即待查找关键字所在结点在B- 树上的层次树, 是决定B树查找效率的首要 因素,对于有n个关键字的m阶B-树,从根结点到关键字所在结点的路径上路过的结点数不超过: 

 

 

4、B-树的插入

 

其实B-树的插入是很简单的,它主要是分为如下的两个步骤:

 1. 使用之前介绍的查找算法查找出关键字的插入位置,如果我们在B-树中查找到了关键字,则直接返回。否则它一定会失败在某个最底层的终端结点上。
 2.然后,我就需要判断那个终端结点上的关键字数量是否满足:n<=m-1,如果满足的话,就直接在该终端结点上添加一个关键字,否则我们就需要产生结点的“分裂”。
     分裂的方法是:生成一新结点。把原结点上的关键字和k(需要插入的值)按升序排序后,从中间位置把关键字(不包括中间位置的关键字)分成两部分。左部分所含关键字放在旧结点中,右部分所含关键字放在新结点中,中间位置的关键字连同新结点的存储位置插入到父结点中。如果父结点的关键字个数也超过(m-1),则要再分裂,再往上插。直至这个过程传到根结点为止。
 

 

下面我们来举例说明,首先假设这个B-树的阶为:3。树的初始化时如下: 

首先,我需要插入一个关键字:30,可以得到如下的结果: 

再插入26,得到如下的结果:

OK,此时如图所示,在插入的那个终端结点中,它的关键字数已经超过了m-1=2,所以我们需要对结点进分裂,所以我们先对关键字排序,得到:26 30 37 ,所以它的左部分为(不包括中间值):26,中间值为:30,右部为:37,左部放在原来的结点,右部放入新的结点,而中间值则插入到父结点,并且父结点会产生一个新的指针,指向新的结点的位置,如下图所示: 


OK,然后我们继续插入新的关键字:85,得到如下图结果: 


正如图所示,我需要对刚才插入的那个结点进行“分裂”操作,操作方式和之前的一样,得到的结果如下: 


哦,当我们分裂完后,突然发现之前的那个结点的父亲结点的度为4了,说明它的关键字数超过了m-1,所以需要对其父结点进行“分裂”操作,得到如下的结果: 


好,我们继续插入一个新的关键字:7,得到如下结果: 


同样,需要对新的结点进行分裂操作,得到如下的结果: 

到了这里,我就需要继续对我们的父亲结点进行分裂操作,因为它的关键字数超过了:m-1. 


哦,终于遇到这种情况了,我们的根结点出现了关键子数量超过m-1的情况了,这个时候我们需要对父亲结点进行分列操作,但是根结点没父亲啊,所以我们需要重新创建根结点了。 


好了,到了这里我们也知道怎么进行B-树的插入操作。

 

 

 

5、B-树的删除操作

 

B-树的删除操作同样是分为两个步骤:

1. 利用前述的B-树的查找算法找出该关键字所在的结点。然后根据 k(需要删除的关键字)所在结点是否为最下层的非终端结点有不同的处理方法。如果没有找到,则直接返回。
2. 若该结点为非终端结点,且被删关键字为该结点中第i个关键字key[i],则可从指针son[i]所指的子树中找出最小关键字Y(位于最下层的非终端结点),代替key[i]的位置,然后在叶结点中删去Y。

如果是最下层的非终端结点的话,需要分为下面三种情况进行删除。

  • 如果被删关键字所在结点的原关键字个数n>=[m/2] ( 上取整),说明删去该关键字后该结点仍满足B-树的定义。这种情况最为简单,只需删除对应的关键字:k和指针:A 即可。
  • 如果被删关键字所在结点的关键字个数n等于( 上取整)[ m/2 ]-1,说明删去该关键字后该结点将不满足B-树的定义,需要调整。

调整过程为:如果其左右兄弟结点中有“多余”的关键字,即与该结点相邻的右兄弟(或左兄弟)结点中的关键字数目大于( 上取整)[m/2]-1。则可将右兄弟(或左兄弟)结点中最小关键字(或最大的关键字)上移至双亲结点。而将双亲结点中小(大)于该上移关键字的关键字下移至被删关键字所在结点中。

  • 被删关键字所在结点和其相邻的兄弟结点中的关键字数目均等于(上取整)[m/2]-1。假设该结点有右兄弟,且其右兄弟结点地址由双亲结点中的指针Ai所指,则在删去关键字之后,它所在结点中剩余的关键字和指针,加上双亲结点中的关键字Ki一起,合并到 Ai所指兄弟结点中(若没有右兄弟,则合并至左兄弟结点中)。

 

下面,我们给出删除叶子结点的三种情况: 
第一种:关键字的数不小于(上取整)[m/2],如下图删除关键字:12 


 
删除12后的结果如下,只是简单的删除关键字12和其对应的指针。 


第二种:关键字个数n等于( 上取整)[ m/2 ]-1,而且该结点相邻的右兄弟(或左兄弟)结点中的关键字数目大于( 上取整)[m/2]-1。 


如上图,所示,我们需要删除50这个关键字,所以我们需要把50的右兄弟中最小的关键字:61上移到其父结点,然后替换小于61的关键字53的位置,53则放至50的结点中。然后,我们可以得到如下的结果: 


第三种:关键字个数n等于( 上取整)[ m/2 ]-1,而且被删关键字所在结点和其相邻的兄弟结点中的关键字数目均等于(上取整)[m/2]-1

如上图所示,我们需要删除53,那么我们就要把53所在的结点其他关键字(这里没有其他关键字了)和父亲结点的61这个关键字一起合并到70这个关键字所占的结点。得到如下所示的结果: 


Ok,我已经分别对上述的四种删除的情况都做了举例,大家如果还有什么不清楚的,可以看看代码,估计就可以明白了

 

 

 

代码

 

记录类型:
 
 
Node 结点向量类型:
 
m B_ 树结点及指针类型:
 
 
B_ 树的查找结果类型:
 
 
 
 
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Boolean; /* Boolean是布尔类型,其值是TRUE或FALSE */

#include<malloc.h> /* malloc()等 */
#include<stdio.h> /* EOF(=^Z或F6),NULL */
#include<process.h> /* exit() */

/* 函数结果状态代码 */
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2 


#define m 3 /* B树的阶,暂设为3 */
#define N 16 /* 数据元素个数 */
#define MAX 5 /* 字符串最大长度+1 */
typedef int KeyType; /* 设关键字域为整型 */
typedef struct
{
	char info[MAX];
}Others;/* 记录的其它部分 */


/* --------------------------------     B-树的结点类型     ----------------------------------*/

typedef struct
{
	KeyType key; /* 关键字 */
	Others others; /* 其它部分(由主程定义) */
}Record; /* 记录类型 */

typedef struct BTNode
{
	int keynum; /* 结点中关键字个数,即结点的大小 */
	struct BTNode *parent; /* 指向双亲结点 */
	struct Node /* 结点向量类型 */
	{
		KeyType key; /* 关键字向量 */
		struct BTNode *ptr; /* 子树指针向量 */
		Record *recptr; /* 记录指针向量 */
	}node[m + 1]; /* key,recptr的0号单元未用 */
}BTNode, *BTree; /* B树结点和B树的类型 */

typedef struct
{
	BTNode *pt; /* 指向找到的结点 */
	int i; /* 1..m,在结点中的关键字序号 */
	int tag; /* 1:查找成功,O:查找失败 */
}Result; /* B树的查找结果类型 */


/* ---------------------------------------------------------------------------------------------*/


/* --------------------------------   动态查找表(B-树)的基本操作   -------------------------------*/


Status InitDSTable(BTree *DT)
{ /* 操作结果: 构造一个空的动态查找表DT */
	*DT = NULL;
	return OK;
}

void DestroyDSTable(BTree *DT)
{ /* 初始条件: 动态查找表DT存在。操作结果: 销毁动态查找表DT */
	int i;
	if (*DT) /* 非空树 */
	{
		for (i = 0; i <= (*DT)->keynum; i++)
			DestroyDSTable(&(*DT)->node[i].ptr); /* 依次销毁第i棵子树 */
		free(*DT); /* 释放根结点 */
		*DT = NULL; /* 空指针赋0 */
	}
}

int Search(BTree p, KeyType K)
{ /* 在p->node[1..keynum].key中查找i,使得p->node[i].key≤K<p->node[i+1].key */
	int i = 0, j;
	for (j = 1; j <= p->keynum; j++)
		if (p->node[j].key <= K)
			i = j;
	return i;
}

Result SearchBTree(BTree T, KeyType K) //算法9.13 
{ /* 在m阶B树T上查找关键字K,返回结果(pt,i,tag)。若查找成功,则特征值 */
  /* tag=1,指针pt所指结点中第i个关键字等于K;否则特征值tag=0,等于K的 */
  /* 关键字应插入在指针Pt所指结点中第i和第i+1个关键字之间。*/
	BTree p = T, q = NULL; /* 初始化,p指向待查结点,q指向p的双亲 */
	Status found = FALSE;
	int i = 0;
	Result r;
	while (p && !found)
	{
		i = Search(p, K); /* p->node[i].key≤K<p->node[i+1].key */
		if (i > 0 && p->node[i].key == K) /* 找到待查关键字 */
			found = TRUE;
		else
		{
			q = p;
			p = p->node[i].ptr;
		}
	}
	r.i = i;
	if (found) /* 查找成功 */
	{
		r.pt = p;
		r.tag = 1;
	}
	else /*  查找不成功,返回K的插入位置信息 */
	{
		r.pt = q;
		r.tag = 0;
	}
	return r;
}

void Insert(BTree *q, int i, Record *r, BTree ap)
{ /* 将r->key、r和ap分别插入到q->key[i+1]、q->recptr[i+1]和q->ptr[i+1]中 */
	int j;
	for (j = (*q)->keynum; j > i; j--) /* 空出q->node[i+1] */
		(*q)->node[j + 1] = (*q)->node[j];
	(*q)->node[i + 1].key = r->key;
	(*q)->node[i + 1].ptr = ap;
	(*q)->node[i + 1].recptr = r;
	(*q)->keynum++;
}

void split(BTree *q, BTree *ap)
{ /* 将结点q分裂成两个结点,前一半保留,后一半移入新生结点ap */
	int i, s = (m + 1) / 2;
	*ap = (BTree)malloc(sizeof(BTNode)); /* 生成新结点ap */
	(*ap)->node[0].ptr = (*q)->node[s].ptr; /* 后一半移入ap */
	for (i = s + 1; i <= m; i++)
	{
		(*ap)->node[i - s] = (*q)->node[i];
		if ((*ap)->node[i - s].ptr)
			(*ap)->node[i - s].ptr->parent = *ap;
	}
	(*ap)->keynum = m - s;
	(*ap)->parent = (*q)->parent;
	(*q)->keynum = s - 1; /* q的前一半保留,修改keynum */
}

void NewRoot(BTree *T, Record *r, BTree ap)
{ /* 生成含信息(T,r,ap)的新的根结点*T,原T和ap为子树指针 */
	BTree p;
	p = (BTree)malloc(sizeof(BTNode));
	p->node[0].ptr = *T;
	*T = p;
	if ((*T)->node[0].ptr)
		(*T)->node[0].ptr->parent = *T;
	(*T)->parent = NULL;
	(*T)->keynum = 1;
	(*T)->node[1].key = r->key;
	(*T)->node[1].recptr = r;
	(*T)->node[1].ptr = ap;
	if ((*T)->node[1].ptr)
		(*T)->node[1].ptr->parent = *T;
}

void InsertBTree(BTree *T, Record *r, BTree q, int i)
{ /* 在m阶B树T上结点*q的key[i]与key[i+1]之间插入关键字K的指针r。若引起 */
  /* 结点过大,则沿双亲链进行必要的结点分裂调整,使T仍是m阶B树。算法9.14改 */
	BTree ap = NULL;
	Status finished = FALSE;
	int s;
	Record *rx;
	rx = r;
	while (q && !finished)
	{
		Insert(&q, i, rx, ap); /* 将r->key、r和ap分别插入到q->key[i+1]、q->recptr[i+1]和q->ptr[i+1]中 */
		if (q->keynum < m)
			finished = TRUE; /* 插入完成 */
		else
		{ /* 分裂结点*q */
			s = (m + 1) / 2;
			rx = q->node[s].recptr;
			split(&q, &ap); /* 将q->key[s+1..m],q->ptr[s..m]和q->recptr[s+1..m]移入新结点*ap */
			q = q->parent;
			if (q)
				i = Search(q, rx->key); /* 在双亲结点*q中查找rx->key的插入位置 */
		}
	}
	if (!finished) /* T是空树(参数q初值为NULL)或根结点已分裂为结点*q和*ap */
		NewRoot(T, rx, ap); /* 生成含信息(T,rx,ap)的新的根结点*T,原T和ap为子树指针 */
}

void TraverseDSTable(BTree DT, void(*Visit)(BTNode, int))
{ /* 初始条件: 动态查找表DT存在,Visit是对结点操作的应用函数 */
  /* 操作结果: 按关键字的顺序对DT的每个结点调用函数Visit()一次且至多一次 */
	int i;
	if (DT) /* 非空树 */
	{
		if (DT->node[0].ptr) /* 有第0棵子树 */
			TraverseDSTable(DT->node[0].ptr, Visit);
		for (i = 1; i <= DT->keynum; i++)
		{
			Visit(*DT, i);
			if (DT->node[i].ptr) /* 有第i棵子树 */
				TraverseDSTable(DT->node[i].ptr, Visit);
		}
	}
}


/* --------------------------------------------------------------------------------------------------*/


void print(BTNode c, int i) /* TraverseDSTable()调用的函数 */
{
	printf("(%d,%s)", c.node[i].key, c.node[i].recptr->others.info);
}

void main()
{
	Record r[N] = { {24,"1"},{45,"2"},{53,"3"},{12,"4"},{37,"5"},
				 {50,"6"},{61,"7"},{90,"8"},{100,"9"},{70,"10"},
				 {3,"11"},{30,"12"},{26,"13"},{85,"14"},{3,"15"},
		 {7,"16"} }; /* (以教科书中图9.16为例) */
	BTree t;
	Result s;
	int i;
	InitDSTable(&t);
	for (i = 0; i < N; i++)
	{
		s = SearchBTree(t, r[i].key);
		if (!s.tag)
			InsertBTree(&t, &r[i], s.pt, s.i);
	}
	printf("按关键字的顺序遍历B_树:\n");
	TraverseDSTable(t, print);
	printf("\n请输入待查找记录的关键字: ");
	scanf("%d", &i);
	s = SearchBTree(t, i);
	if (s.tag)
		print(*(s.pt), s.i);
	else
		printf("没找到");
	printf("\n");
	DestroyDSTable(&t);
}

运行结果:

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值