B树概念及操作笔记(含完整代码实现)

二叉树------顺序性------> BST------平衡性------>AVL树------>自平衡

AVL树的缺点

不直观 + 不适合动态查找 + 不适用于外部查找

B树:多路平衡查找树------>绝对平衡(平衡因子为0) + 一个结点中可以存放多个数据(一个结点可用于多棵子树)

B树是怎么来的?

因为内外存交互比较慢,一个结点中可以存放多个数据,一次性从外存中取出多个数据可以减少访问外存的次数。

但万一数据量太大了怎么办?B树可以只存储数据的关键字(如地址),通过这个索引找到外存中的数据。

2-3树:要么是一棵空树,要么由以下结点组成:

2-结点:含有一个数据和两棵子树,左子树的数据<根节点<右子树的数据

3-结点:含有两个数据和三棵子树,子树与根节点之间也要满足顺序性

满足顺序性,并且要求绝对平衡

例如,如下三个结点是3-结点

顺序性:父亲结点中的多个数据插到子树之间,满足顺序性即可

2-3-4树

要么是一棵空树,要么由以下结点组成:

2-结点:含有一个数据和两棵子树,左子树的数据<根节点<右子树的数据

3-结点:含有两个数据和三棵子树,子树与根节点之间也要满足顺序性

4-结点:含有三个数据和四棵子树,子树与根节点之间也要满足顺序性

要求绝对平衡

2-3-4-5-...,m叉树

在这里,把度叫做"阶"

m阶的多路平衡查找树------m阶B树------>绝对平衡

n个结点,m阶B树,高度最小为 logmn(mk=n,k=logmn)

应用:管理数据,多用于数据库

缺点:但是插入和删除算法比较复杂


B树 要求绝对平衡

m阶B树------m路平衡查找树

上图是四阶B树,空孩子是叶子节点。

满二叉树==2阶B树

细节:

  1. m阶的B树一定要有度为m的节点吗?不一定。

m阶B树是指树中每个节点最多有m个孩子,不代表一定有m

比如这幅图你说它是5阶B树也是对的,但是平时习惯找离得最近的

  1. 数据(记录)由多个数据项组成,每个数据项都不同的叫做主键/数据的关键字

索引存储------用B树索引存储------存储关键字和地址

我们并不是要完整地存储数据,数据的位置还是在外存中,我们需要存储数据的关键字以及地址addr,关键字定位到地址,

进而找到它在外存中的位置。(关键字,地址)------>索引项

但是为了简化,我们这里认为一个数据只有一个数据项,也没有存储外存地址的属性

  1. 一棵m阶的B树性质:

(1)树中每个节点最多有m个孩子和m-1个数据,数据个数永远比孩子少一个

(2)为了起到优化作用(一次能取出多个数据),控制树高,减少查找次数,这里对B树节点数目的下限做了要求

根节点:至少有1个数据和2个孩子,

其他非根节点:最少ceil(m/2)个孩子,ceil(m/2)-1个关键字。最多有m个孩子,m-1个关键字

也就是说除了根节点,其他结点至少有阶数一半的孩子结点

(3)顺序性:中序遍历序列有序(左子树 根节点中的一个数据 右子树)

遍历顺序

1,2分别是12的左右孩子,2,3分别是22的左右孩子

(4)平衡性:绝对平衡,内部终端节点都在同一层,所有的叶子节点都在同一层

复习:内部终端节点=孩子全为空的节点,叶子节点=空节点=失败节点


操作:查找 插入 删除

先明确一点,如果我要插入一个14,能不能像之前那样新建一个节点,再把它插进去?

不行,破坏了绝对平衡,要插每个终端节点都得插。所以我们的想法是让14插入到原本的终端节点中

1.查找:查找关键字是否有x?

基于顺序性,找是否存在第一个大于等于x的数据y:

如果y==x,查找成功;如果x<y,往y的左子树中找。如果不存在就往y的右子树找

直到y==x或者走到了叶子节点(查找失败)

2.插入操作:

在B树中插入数据x

法一:回溯插入法:从上往下查找,从下往上调整

(1)找到x应该在的位置<某个终端节点,数据数组的下标>

执行查找x的操作,在查找的操作上多了一个指针pre记录父亲节点,当p走到叶子节点时,pre指向的终端节点就是

x应该插入到的位置,然后pre节点中的一个大于x的数据的下标是i,那么x就应该插入到i位置

其实插入一个数据x会连带着多了一个孩子,但由于孩子是叶子节点,不会破坏性质

(2)节点z中多了一个数据,可能会超过数量上限(m-1),因此判断是否需要调整

  • 如果插入x后,节点z的数据量仍然是小于等于m-1,不需要调整
  • 如果插入x后,节点z的数据量是m,需要调整,分裂向上插入。

均分是为了保证不低于下限

分裂向上插入:从下标s位置分开,s=ceil(m/2)位置分开:data1~datas-1 datas datas+1~datam

data1~datas-1及其对应的孩子保留在z节点中,剩下的数据及其对应的孩子在z节点中删除

把datas+1~datam及其对应的孩子保存在新建节点ap中,然后我们把数据datas及其新节点ap插入到z的父亲节点中。

(往父亲节点中插入了一个数据后就多了一个孩子,把ap当作新孩子)

还有,均分是为了保证不低于下限

下面以插入10为例:

但是如果插入父亲之后也有可能超过上限啊!嗯,然后把原来的父亲节点作为插入的节点,同样操作到父亲的父亲节点...

循环,直到父亲节点多一个节点之后不超过上限或者父亲节点为根节点并且超过上限------>

把根节点分裂,再添加一个根节点

还是同样的,data1~datas-1保留,新建一个节点:datas+1~datam,再新建一个根节点存datas

法二:主动分裂法:不用回溯,但是可能会把不会满的节点也给分裂了~

(1)找到x应该在的位置<某个终端节点,数据数组的下标>

(2)在寻找插入位置的同时,把经过的分裂风险的节点(有m-1个节点)顺便分裂。

总结 插入x:

如果是空树,直接新建一个节点,把数据x装进去

如果不是空树,查找到x应该在的位置<所在的节点,数组中的下标> 两个指针:p和pre

  • 如果x已经存在,不需要插入
  • 如果p指针指向了叶子节点,那么pre指针指向的节点就是数据x应该在的位置,把x插入到节点当中
    • 如果节点数据个数没有超过上限,结束
    • 如果节点个数超过上限,需要调整:从s=ceil(m/2)中分裂,

分为data1~datas-1 datas datas+1~datam(因为多了一个)三部分

前者保留,datas插入到父亲节点的关键字数组中,后者新建一个节点ap放入其中,并把ap插入到节点指针数组中

既要插入关键字,也要插入新的孩子指针啊------而且是右孩子哦~

继续循环向上调整

直到没有再超过上限或者到达了根节点并且超过上限

  • 到达了根节点并且超过上限:同样的分裂操作,只是父亲节点是新建的根节点,也就是说这里要新建两个节点

于是我们来推一推节点的结构,m阶B树

一个节点最多有m-1关键字,所以我们用一个数组存储关键字。同样的,一个节点最多有m个孩子,所以我们用一个数组存储地址

然后需要有个变量存储关键字的具体数量

如果用回溯插入法,需要找父亲,所以还需要存储父亲的地址

神奇的分裂------既要分数据,也要分孩子

3.删除

在B树中删除数据x------>少一个数据也少一个孩子

(1)找到x的位置------<节点z,datai>

执行查找操作

(2)执行删除:分类讨论,分两种情况

  • z是终端节点,孩子都是NULL,并且节点数据数量也不可能为0,直接删除datai,不需要特殊处理孩子

把datai+1~datan(假设n个数据)和chi+1~chn往前移动即可

此时z中少一个数据,可能会低于下限ceil(m/2)-1(根节点删掉成空树了):

  • 如果z是根节点低于下限(1),那就成空树了,直接delete掉
  • 如果z不是根节点并且低于下限ceil(m/2)-1,进行调整:其余情况不进行操作

看左右相邻的亲兄弟:

  • 如果有一个兄弟的关键字个数大于下限ceil(m/2)-1,找该兄弟借一个关键字

为了保证顺序性,如果是左兄弟要借最大值,右兄弟要借最小值。并且两个节点夹着的父亲的数据也要换下来

比如删掉15,把9拿到父亲节点,两个节点夹着的数据12拿到下面的节点

  • 如果有两个兄弟的关键字刚好是下限,合并其中一个兄弟,把父亲中对应的数据也合并进来。

我们决定要让靠右的合并到靠左的,这样数据就可以直接放到左数组后面

此时父亲少了一个数据和一个孩子,可能低于下限

  • 再对父亲节点进行调整。 循环向上调整 直到父亲节点中数据量不低于下限,或者父亲节点是根节点停止。

其中如果父亲节点是根节点并且只有一个数据, 合并后,要换根

  • 如果是非终端节点,删除一个数据也要删除一个孩子,但是我们不能直接把孩子给删掉啊

做法:找x的中序遍历前驱数据/后继数据k,用k顶替x(就像是数组中删除使用后面覆盖前面数据一样,这首先是一个BST树)

于是就是去终端节点上删除k,转化为了情况1-->去终端节点删除key1

ps:建议找后继(右子树中最靠左的数据) 因为最靠左的数据下标一定是1,节点下标一定是0

完整代码实现:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#define m 5
#define minn (m+1)/2-1//非根结点的关键字下限 ceil(m/2)-1------>根结点的关键字下限是1

typedef struct BTNode
{
	int key[m+1];//数据数组:从下标1开始存数据,考虑到先插入后分裂,那么分裂之前要能存下m个数据 因此k[1]~~k[m]
	struct BTNode* ptr[m+1];//孩子指针数组:分裂之前最多有m+1个孩子,从下标0开始存,ptr[0]~~ptr[m]
	//key[i]对应的左孩子是ptr[i-1] 右孩子是ptr[i]
	int keynum;//结点中数据的真实个数
	struct BTNode* fa;//指向父亲结点指针
}BTNode,*BTree;
typedef struct
{
	BTNode* z;//指向所在的结点
	int i;//数据数组的下标
	int tag;//是否查找成功 ==0失败 ==1成功
}Result;
void Restore(BTree &t,BTNode* z);
void Meger(BTree &t,BTNode* &pa,BTNode* x,BTNode* &y,int yi);
void PrintfBTree(BTree t,int tab)//输出B树
{
	if(t==NULL)return;
	int i=0;
	for(i=1;i<=tab;i++)
	{
		printf("	");
	}
	//输出结点中的数据
	for(i=1;i<=t->keynum;i++)
	{
		printf("%d ",t->key[i]);
	}
	printf("\n");
	for(i=0;i<=t->keynum;i++)
	{
		PrintfBTree(t->ptr[i],tab+1);
	}
}
int Search(BTNode* p,int k)
{
	//在p结点的数据数组中找第一个大于等于k的位置
	int i;
	for(i=1;i<=p->keynum;i++)
	{
		if(p->key[i]>=k)
		{
			break;
		}
	}
	return i;

}
void SearchBTree(BTree t,int k,Result &r)
{
	//从根结点开始查找,查找过程中保存父亲
	BTNode* p=t;//从根开始
	BTNode* pre=NULL;//查找过程中保存p的父亲
	int f=0;//是否找到k
	int i=0;
	while(p!=NULL&&f==0)
	{
		i=Search(p,k);//在结点p中 找第一个大于等于k的位置
		if(i<=p->keynum&&p->key[i]==k)//注意 一定要判断越界
		{
			f=1;//找到k了
		}
		else
		{
			pre=p;
			p=p->ptr[i-1];//往key[i]的左孩子走。
		}

	}
	if(f==1)
	{//找到了
		r.z=p;//k在的结点
		r.i=i;
		r.tag=1;

	}
	else
	{//没有找到k 即k不存在
		r.z=pre;//k应该插入的结点
		r.i=i;//k应该在的位置
		r.tag=0;
	}

}
BTNode* NewRoot(BTNode* p,int k,BTNode* ap)
{
	BTNode* s=(BTNode*)malloc(sizeof(BTNode));
	s->key[1]=k;
	s->ptr[0]=p;
	s->ptr[1]=ap;
	s->keynum=1;
	if(p!=NULL)p->fa=s;
	if(ap!=NULL)ap->fa=s;
	s->fa=NULL;//根结点没有父亲
	return s;
}
void Insert(BTNode* z,int i,int k,BTNode* ap)
{
	//把key[i]~~key[keynum]挨个后移 空出key[i]
	//把ptr[i]~~ptr[keynum]挨个后移 空出ptr[i]
	for(int j=z->keynum;j>=i;j--)
	{
		z->key[j+1]=z->key[j];
		z->ptr[j+1]=z->ptr[j];
	}
	z->key[i]=k;
	z->ptr[i]=ap;
	if(ap!=NULL)ap->fa=z;
	z->keynum++;//注意
}
void Split(BTNode* z,int s,BTNode* &ap)
{
	//key[1]~~key[s-1]留在z中,key[s+1]~~key[m]给ap
	//ptr[0]~~ptr[s-1]留在z中,ptr[s]~~ptr[m]给ap
	ap=(BTNode*)malloc(sizeof(BTNode));
	int j=0;//ap结点的下标
	ap->ptr[0]=z->ptr[s];
	if(z->ptr[s]!=NULL)z->ptr[s]->fa=ap;//改一下bug 不是z->ptr[0]
	for(int i=s+1;i<=m;i++)//枚举z中给ap的数据和孩子
	{
		j++;
		ap->key[j]=z->key[i];
		ap->ptr[j]=z->ptr[i];
		if(z->ptr[i]!=NULL)z->ptr[i]->fa=ap;
	}
	ap->keynum=j;//j=m-s;
	z->keynum=s-1;
}
void InsertBTree(BTree &t,int k,BTNode* z,int i)
{//在以t为根的B树中插入数据k 插入位置:结点z的key[i];
	//空树插入
	if(t==NULL)
	{
		t=NewRoot(NULL,k,NULL);
		return ;
	}
	//非空树插入:在结点z-》key[i]插入k------->同时也要在孩子数组的i位置插入一个孩子结点
	BTNode* ap=NULL;
	while(1)
	{
		//在k插入到z结点的key[i] 把ap插入到z结点的ptr[i]
		Insert(z,i,k,ap);
		if(z->keynum<=m-1)
		{
			break;//插入后不超过上限,结束。
		}
		else
		{//分裂上传
			int s=(m+1)/2;//等同于ceil(m/2);
			k=z->key[s];//等下继续循环往父亲结点中插入k
			Split(z,s,ap);
			//把k和ap  插入到z的父亲结点中
			if(z->fa!=NULL)
			{
				z=z->fa;//z指向新的被插入的结点
				i=Search(z,k);//查找插入位置。
				//继续循环 执行插入。
			}
			else
			{
				t=NewRoot(z,k,ap);
				break;//根结点分裂 结束

			}

		}

	}

}
void InsertKeyOperation(BTree &t)
{
	int k;
	Result r;
	while(1)
	{
		printf("请给出需要插入的数据:\n");
		scanf("%d",&k);
		SearchBTree(t,k,r);
		if(r.tag==1)
		{
			printf("该关键字已经存在,不能再次插入\n");
		}
		else
		{
			InsertBTree(t,k,r.z,r.i);//在r.z结点的数据数组中 插入k 插入到r.i下标位置
			printf("插入成功,B树如下: \n");
			printf("-----------------------------------\n");
			PrintfBTree(t,1);//输出B树
			printf("-----------------------------------\n");
		}
		printf("如果要继续插入数据请输入y\n");
		char c;
		getchar();
		scanf("%c",&c);
		if(c!='y')
		{
			break;
		}

	}

}
//-----------------------删除操作-------------------
void Remove(BTNode* z,int i)
{//从z结点中删除key[i]和ptr[i]
	//把key[i+1]~~~key[keynum]挨个前移
	//把ptr[i+1]~~~ptr[keynum]挨个前移
	for(int j=i+1;j<=z->keynum;j++)
	{
		z->key[j-1]=z->key[j];
		z->ptr[j-1]=z->ptr[j];
	}
	z->keynum--;//注意
}
void Borrow(BTNode* z,BTNode* lbro,BTNode* rbro,BTNode* pa,int i)
{
	//i是z的下标 也就是z是pa->ptr[i].
	if(lbro!=NULL&&lbro->keynum>=minn+1)
	{//左兄弟存在且够借
		//z结点中 空出key[1]和ptr[0]
		for(int j=z->keynum;j>=1;j--)
		{
			z->key[j+1]=z->key[j];
		}
		for(int j=z->keynum;j>=0;j--)
		{
			z->ptr[j+1]=z->ptr[j];
		}
		//借关键字,父亲结点pa的key[i]给z->key[1],左兄弟结点的lbro->key[lbro->keynum]给父亲pa->key[i]
		z->key[1]=pa->key[i];
		pa->key[i]=lbro->key[lbro->keynum];
		z->ptr[0]=lbro->ptr[lbro->keynum];
		if(z->ptr[0]!=NULL)z->ptr[0]->fa=z;
		z->keynum++;
		lbro->keynum--;
	}
	else
	{//找右兄弟借
		//把父亲中的key[i+1]给z
		//右兄弟的key[1]给父亲的key[i+1]
		//右兄弟的ptr[0]给z
		z->key[z->keynum+1]=pa->key[i+1];
		pa->key[i+1]=rbro->key[1];
		z->ptr[z->keynum+1]=rbro->ptr[0];
		if(z->ptr[z->keynum+1]!=NULL)z->ptr[z->keynum+1]->fa=z;
		//处理右兄弟
		for(int j=2;j<=rbro->keynum;j++)
		{
			rbro->key[j-1]=rbro->key[j];
		}
		for(int j=1;j<=rbro->keynum;j++)
		{
			rbro->ptr[j-1]=rbro->ptr[j];
		}
		z->keynum++;
		rbro->keynum--;

	}

}
void Meger(BTree &t,BTNode* &pa,BTNode* x,BTNode* &y,int yi)
{//结点x和结点y合并 pa是x和y的父亲 yi是y结点的下标
	//把右边的结点y 合并到其左兄弟x中去
	//合并关键字和孩子
	//父亲结点的key[yi]给x。然后y中的数据key[1]~~~key[y->keynum]给x。y中的孩子ptr[0]~~~ptr[y->keynum]给x
	x->key[x->keynum+1]=pa->key[yi];
	x->ptr[x->keynum+1]=y->ptr[0];
	if(y->ptr[0]!=NULL)y->ptr[0]->fa=x;
	x->keynum++;
	for(int i=1;i<=y->keynum;i++)
	{
		x->keynum++;
		x->key[x->keynum]=y->key[i];
		x->ptr[x->keynum]=y->ptr[i];
		if(y->ptr[i]!=NULL)y->ptr[i]->fa=x;
	}

	//处理y结点
	free(y);
	y=NULL;
	pa->ptr[yi]=NULL;
	//处理父亲结点
	//删除父亲结点中的key[yi]和ptr[yi]
	Remove(pa,yi);
	//判断父亲结点是否需要调整
	if(pa->fa==NULL)
	{//pa是根结点
		if(pa->keynum<1)
		{
			t=x;//根结点是唯一的孩子x
			x->fa=NULL;
			free(pa);
			pa=NULL;
		}

	}
	else
	{//pa不是根结点
		if(pa->keynum<minn)
		{
			Restore(t,pa);//调用非根结点的调整函数。
		}

	}

}
void Restore(BTree &t,BTNode* z)
{//B树中 非根结点z数据量低于下限 需要调整
	//先确定z的左右兄弟
	BTNode* pa=z->fa;//z的父亲结点
	int i;//z结点是在其父亲结点的孩子数组中的下标是i
	for(i=0;i<=pa->keynum;i++)//枚举pa的孩子,找z结点的下标
	{
		if(pa->ptr[i]==z)
		{
			break;
		}
	}

	BTNode* lbro=NULL;//z的左兄弟
	BTNode* rbro=NULL;
	if(i>0)lbro=pa->ptr[i-1];
	if(i<pa->keynum)rbro=pa->ptr[i+1];

	if((lbro!=NULL&&lbro->keynum>=minn+1)||(rbro!=NULL&&rbro->keynum>=minn+1))
	{//至少有一个兄弟够借
		Borrow(z,lbro,rbro,pa,i);//借关键字
	}
	else
	{//没有兄弟可以借关键字---》合并
		BTNode* x=NULL;//两个结点合并 x指向靠左的
		BTNode* y=NULL;//两个结点合并 x指向靠右的
		int yi=0;//保存y结点左父亲结点的孩子数组中的下标
		if(lbro!=NULL)
		{//左兄弟存在 z和左兄弟合并
			x=lbro;
			y=z;
			yi=i;

		}
		else
		{//z和右兄弟合并
			x=z;
			y=rbro;
			yi=i+1;
		}
		Meger(t,pa,x,y,yi);//合并
	}

}
void DeleteBtree(BTree &t,BTNode* z,int i)
{//删除B树的结点z中的key[i]和ptr[i]
	if(z->ptr[0]!=NULL)
	{//非终端结点
		//找key[i]的中序遍历后继数据k1,假设k1在结点q中。---》q一定是终端结点
		BTNode* q=z->ptr[i];
		while(q->ptr[0]!=NULL)
		{
			q=q->ptr[0];
		}
		int k1=q->key[1];
		z->key[i]=k1;
		//问题转化为去q结点中删除q->key[1];
		z=q;
		i=1;
	}
	//z一定是终端结点 i也一定是真正被删掉数据所在的位置。
	Remove(z,i);//从z结点中删除key[i]和ptr[i]

	if(z->fa==NULL)
	{//z是根结点
		if(z->keynum<1)
		{//删除之后 树中没有数据了,就是空树
			t=NULL;
			free(z);
			z=NULL;
		}

	}
	else
	{//z不是根结点
		if(z->keynum<minn)
		{
			Restore(t,z);//调整:B树中 结点z数据量低于下限 需要调整
		}

	}

}
void DeleteKeyOperation(BTree &t)
{
	int k;
	Result r;
	while(1)
	{
		printf("请给出需要删除的数据:\n");
		scanf("%d",&k);
		SearchBTree(t,k,r);//查找k

		if(r.tag==0)
		{//查找失败
			printf("该关键字已经不存在,不能执行删除操作\n");
		}
		else
		{//查找成功
			DeleteBtree(t,r.z,r.i);//在r.z结点中删除key[r.i]; 同时也要顺便删掉一个孩子
			printf("删除成功,B树如下: \n");
			printf("-----------------------------------\n");
			PrintfBTree(t,1);//输出B树
			printf("-----------------------------------\n");
		}

		printf("如果要继续删除数据请输入y\n");
		char c;
		getchar();
		scanf("%c",&c);
		if(c!='y')
		{
			break;
		}

	}

}
int main()
{
	
	BTree t=NULL;
	InsertKeyOperation(t);
	DeleteKeyOperation(t);
	return 0;
 } 


/*
	39
		12 22
			5 8 9
			13 15
			295 29 35
		53
			41 50
			97
			
*/
相关推荐
luj_17681 小时前
变频技术核心原理揭秘
服务器·c语言·开发语言·经验分享·算法
rannn_1111 小时前
【力扣hot100】图论专题+模板|DFS、BFS、拓扑排序...
java·算法·leetcode·深度优先·图论
l1258651 小时前
# RAG多轮对话检索设计:Query重写如何让“那它呢“变成完整问题
前端·数据库·人工智能·python·算法·fastapi·milvus
小弥儿1 小时前
GitHub今日热榜 | 2026-08-20:Agent 上下文数据库接棒
数据库·学习·开源·github
sunoo-2291 小时前
【C 语言标准 IO 入门】第二天学习笔记:文件操作核心函数 + 实战案例 + 踩坑合集
linux·笔记·vscode·学习
疯狂打码的少年1 小时前
【数据结构】图的应用:最短路径(Dijkstra / Floyd)
数据结构·笔记·算法
筠筠喵呜喵1 小时前
解决问题:QNX平台调用glReadPixels卡滞
c++·unix
linux-hzh1 小时前
百日算法修炼 · Day 13
算法
倔强的石头_1 小时前
向量数据库从相似度检索走向融合数据底座
数据库