设计B+树(B+Tree)

设计B+树(B+Tree),第1张

目录

设计一个节点结构

原理及解释

代码块

算法设计

插入算法

从叶子结点查找的方式

从根节点查找的方式

insert_Leaf_Item(ptr, pos, kx, rec);

创建叶子结点的

叶子节点分裂转移代码

分支节点插入

好了,插入代码到此结束了;

 测试如图


设计一个节点结构 原理及解释

先可以看看别的地方给出的B+树定义,B+树点击即可;

对于B+树节点结构的思路如下:

现在设计的节点框架如下,看看都要设计什么变量;

typedef struct BNode
{
  //B+树的变量
}BNode;

(1)每个节点必须有存放自己双亲节点指针;

(2)对于关键码来说,必须要知道关键码的个数,后面 *** 作都能用上;

(3)对于B+树来说,叶子节点和分支节点结构不一样吧;所以得区分一下吧;

(4)关键码不可少吧;

(5)记录集指针不可少,我们就是通过关键码找记录集的吧;

(6)对于一个树来说,孩子指针不能少吧;

(7)对于B+树的叶子结点来说,是通过链表连接的,所以链表所用的直接前驱指针和直接后继指针不能不有吧;

到现在基本就结束了;以上述给出个设计思路,给出一张我理解的图,拿图好说话(以五阶B+树为例);

 图中字母意思:

  • n代表 num(关键码个数) 
  • p 代表Parent指针(指向双亲结点)
  • key代表关键码数组(不占位只是为好理解)
  • t代表节点是分支还是叶子;
  • r代表记录集指针数组(r字符不占位置)
  • pr代表链表的前驱指针 
  • nt代表链表后继指针

对于一个节点结构来说,它只能为分支或者是叶子结点,只能是一个,所以设计结构体如下:

typedef struct BNode
{
	int num; //关键码数量
	BNode* parent;//双亲指针
	NodeType utype; // LEAF , BRCH;	结构体类型
	KeyType key[M + 1];//关键码数组
	union
	{
		struct	// LEAF  叶子结点
		{
			Record* recptr[M + 1];
			BNode* prev, * next;
		};
		// BRCH;  分支节点
		BNode* sub[M + 1];
	};
}BNode;

对于B+树来说,我们可以怎样去设计它的结构,为了查找方便设计如下:

typedef struct
{
	struct BNode* root;//指向根节点
	struct BNode* first;//指向叶子结点第一个节点
	int cursize;//结构体数量
}BTree;

最后一步,设计一结构体,用于查询,插入,如下;

typedef struct
{
	struct BNode* pnode;	// 节点指针
	int index;			//节点某一关键码位置
	bool tag;			//是否存在该值
}Result;
代码块

对于五阶B+树来说,整体代码块如下:

#define  M  5   //  M 奇数 // 
#define  BRCHMAX    (M-1) //  // SUB M 
#define  BRCHMIN    (M/2);	  // SUB M/2+1

#define  LEAFMAX (M)   // MAX ELEM	 5
#define  LEAFMIN (M/2+1)   // MIN ELEM; 3 

typedef int KeyType;
typedef struct {}Record;
typedef enum { BRCH = 1, LEAF = 2 } NodeType;

typedef struct BNode
{
	int num; //关键码数量
	BNode* parent;//双亲指针
	NodeType utype; // LEAF , BRCH;	结构体类型
	KeyType key[M + 1];//关键码数组
	union
	{
		struct	// LEAF  叶子结点
		{
			Record* recptr[M + 1];
			BNode* prev, * next;
		};
		// BRCH;  分支节点
		BNode* sub[M + 1];
	};
}BNode;
typedef struct
{
	struct BNode* root;//指向根
	struct BNode* first;//指向叶子结点第一个结构
	int cursize;
}BTree;

typedef struct
{
	struct BNode* pnode;	// 保存节点
	int index;			//保存关键码位置
	bool tag;			//是否有该值
}Result;
算法设计 插入算法

首先我们知道插入一个值得插入关键码和记录集吧;设计插入函数如

bool Insert(BTree& tree, KeyType kx, Record* rec)
{
 、、、、、、
}

我们对于刚刚定义的结构进行插入演示;示例{55 35 25 10 8 60 59 17 85 105 37 44 65};

(1)首先插入的是55,因为B+树的插入只在叶子上进行,所以对于第一个值来说,此时我们先判断根节点是否为空,如果为空,我们得建立一个叶子结点,所以55一定会给叶子结点插入;

if (tree.root == nullptr)
	{
		BNode* s = BuyLeaf();//购买叶子结点
		s->key[0] = kx;
		s->recptr[0] = rec;
		s->num = 1;
		tree.root = tree.first = s;
		return true;
	}

(2)如果root不为空,我们就得查找kx应该插入的位置吧;对于查找位置分为两种情况,从根节点查找,和从叶子结点,还记得我们定义的最后一个结构体没;

从叶子结点查找的方式

需要注意的是,我们每次最好给一个节点的最后插入数据,如果找到的位置是节点的第一个值之前,如果该节点的前驱结点不空,我们就让其指向前驱结点的最后一个值;

Result FindLeaf(BNode* ptr, KeyType kx)
{
	Result res = { nullptr,-1,false };//初始化查找结构体
	BNode* p = ptr;
	while (p != nullptr && p->next != nullptr && kx > p->key[p->num - 1])//防止p等于NULLptr
	{
		p = p->next;
	}
	if (p == nullptr) return res;//如果为nullptr,则直接返回
	int pos = p->num - 1;//从0开始,所以走实际位置
	while (pos >= 0 && kx < p->key[pos])
	{
		--pos;
	}
	res.pnode = p;//记录找到的节点
	res.index = pos;//记录找到的位置
	if (pos < 0 && p->prev != nullptr)//如果是负数,则将其移动到前一个叶子结点,给前一个结点最后
	{
		res.pnode = p->prev;
		res.index = p->prev->num - 1;
	}
	else 	if (pos >= 0 && kx == p->key[pos])
	{
		res.tag = true;
	}
	return res;
}
从根节点查找的方式

思路:从根节点出发,一直找到叶子结点的第一个结构,再使用叶子查找方式;

Result FindRoot(BNode* ptr, KeyType kx)
{
	Result res = { nullptr,-1,false };
	BNode* p = ptr;
	while (p != nullptr && p->utype == BRCH)//找到第一个叶子结点;
	{
		p->key[0] = kx;
		int i = p->num;
		while (kx < p->key[i]) --i;
		p = p->sub[i];
	}

	res = FindLeaf(p, kx);//进行叶子方式查找

	return res;
}

我们现在知道两种方式的查找方式;那就根节点不为空,那就找位置呗;

	Result resr = FindRoot(tree.root, kx);//从根节点查找
	Result resf = FindLeaf(tree.first, kx);//从叶子结点查找
	if (resf.pnode == nullptr)//如果返回值为nullptr
	{
		cout << "Btree struct error " << endl;
		return false;
	}
	if (resf.tag)//如果tag==true则表明有该值
	{
		cout << "已经有该值了" << endl;
		return false;
	}
	BNode* ptr = resf.pnode;
	int pos = resf.index;
	Insert_Leaf_Item(ptr, pos, kx, rec);//进行插入
insert_Leaf_Item(ptr, pos, kx, rec);

那就开始写Insert_Leaf_Item(ptr, pos, kx, rec);代码吧;

void Insert_Leaf_Item(BNode* ptr, int  pos, KeyType  kx, Record* rec)
{
	for (int i = ptr->num - 1; i > pos; --i)//吧该位置的值依次往后移动,留下位置
	{
		ptr->key[i + 1] = ptr->key[i];
		ptr->recptr[i + 1] = ptr->recptr[i];
	}
	ptr->key[pos + 1] = kx;
	ptr->recptr[pos + 1] = rec;
	ptr->num += 1;//num++
}

现在给叶子节点已经插入完成了,但是新的问题出来了,会不会产生分裂?对于五阶B+树来说,每个节点最多四个关键码,第五个进入的时候就要进行分裂;先看图:

假设插入的节点指针是ptr吧;

首先判断ptr->num是否等于5,如果是,也就是关键码满了需要进行分裂;如图,

(1)将LEAFMIN后的值依次赋值给新建的叶子结构体的关键码0 1 2 位置;

(2)把ptr的前驱和后继改一下吧,右兄弟节点了;

(3)判断ptr->parent是否存在,如果不存在,也就是说没有双亲结点吧,那就建造一个分支节点,将ptr->next的0位置的值赋给新建的节点,并且改一下双亲指针,毕竟现在拥有双亲了;在改双亲的sub指针;

(4)如果ptr有双亲指针,也就是人家有爸爸,咋办,再判断,也许爸爸节点的关键码key达到五个了,也许要分裂,所以说慢慢来;

思路出来了 ,那就代码吧,首先判断一下:

if (ptr->num > LEAFMAX)
	{
		BNode* newroot = Splice_Leaf(ptr);
		if (newroot != nullptr)
		{
			tree.root = newroot;
		}
	}

如果分裂之后产生的节

现在开始写分裂代码BNode* Splice_Leaf(BNode* ptr);

首先我们已经确定ptr节点已经满了,需要分裂,那就先买一个叶子结点;

BNode* s = BuyLeaf();
创建叶子结点的

创建叶子结点的代码简单,我直接给出

BNode* Buynode()
{
	BNode* s = (BNode*)malloc(sizeof(BNode));
	if (nullptr == s) exit(1);
	memset(s, 0, sizeof(BNode));
	return s;
}
BNode* BuyLeaf()
{
	BNode* s = Buynode();
	s->parent = nullptr;
	s->utype = LEAF;
	return s;
}

创建好叶子结点后,我们就要开始上图中的第一步,将ptr的关键码转移到性节点s上去:

KeyType kx = Move_Leaf_Item(s, ptr);
叶子节点分裂转移代码

开始写转移代码,将ptr从num到LEAFMIN的值转移到分裂的新节点s

KeyType Move_Leaf_Item(BNode* s, BNode* ptr)
{
	for (int i = 0, j = LEAFMIN; j < ptr->num; ++i, ++j)//循环给s节点赋值
	{
		s->key[i] = ptr->key[j];
		s->recptr[i] = ptr->recptr[j];
	}
	s->num = LEAFMIN;//配置s->num
	ptr->num = LEAFMIN;//更新ptr->num
	s->parent = ptr->parent;//两个节点的双亲指针是相同的,直接赋值
	s->next = ptr->next;//更新兄弟指针(next和prev)
	s->prev = ptr;
	ptr->next = s;
	if (s->next != nullptr)//如果原ptr有后继节点,也就是s是插入新节点,更新s->next
	{
		s->next->prev = s;
	}
	return s->key[0];
}

现在新节点s已经被设置好了,但是新的问题又来了,如图

 当加入60,70时,ptr满了,需要分裂,和上面一样首先创建新的叶子节点,然后看看双亲指针,发现人家不为空,那就不用创建新的了,直接添加就行了吧;

所以我们还得判断,如下

如果prt->parent==nullptr时,我们就去创建新的,如上上图一样;

如果ptr->parent!=nullptr时,我们该怎么办?直接插入,那分支节点也满了呢?

那就先说没满的情况吧;

    BNode* pa = ptr->parent;
	int pos = pa->num;
	pa->key[0] = kx; //
	while (pos > 0 && kx < pa->key[pos]) { --pos; }//找到对应的插入分支节点的位置
	Insert_Brch_Item(pa, pos, kx, s);
分支节点插入

开始分支节点插入的代码吧,很简单:

void Insert_Brch_Item(BNode* ptr, int pos, KeyType kx, BNode* right)
{
	for (int i = ptr->num; i > pos; --i)
	{
		ptr->key[i + 1] = ptr->key[i];
		ptr->sub[i + 1] = ptr->sub[i];
	}
	ptr->key[pos + 1] = kx;
	ptr->sub[pos + 1] = right;	 // right->parent;
	ptr->num += 1;
}

好了,快结束了。那如果我们给分支节点插入了一个值之后,pa->num=5了,是不是分支需要分裂了;看图:

  如图当ptr节点分裂时会产生新的节点qtr,和新的分支关键码,但是对于分支pa来说,已经达到5个值,需要分裂;

 如上图,pa分支节点,分裂为两个分支节点,如图

那代码还需要判断一下

	if (pa->num > BRCHMAX)
	{
		return Splice_Brch(pa);
	}
	else
	{
		return nullptr;
	}

现在开始写BNode* Splice_Brch(BNode* ptr);

首先注意后面为递归形式,可以这样想,分裂之后是不是有两种情况

  • (1)ptr->parent==nullptr要么产生一个一个新的分支节点,
  • (2)ptr->parent!=nullptr,需要给ptr的双亲结点增加一个关键码,又有新的问题,如果增加的关键码导致该分支结构满了,又要分裂吧;所以用递归
BNode* Splice_Brch(BNode* ptr)
{
	BNode* s = BuyBrchnode();//创建一个分支节点
	KeyType kx = Move_Brch_Item(s, ptr);//移动分支节点的值
	if (ptr->parent == nullptr)
	{
		return MakeRoot(kx, ptr, s);//如果分支ptr->parent==nullptr得创建吧
	}

	BNode* pa = ptr->parent;
	int pos = pa->num;
	pa->key[0] = kx; //
	while (pos > 0 && kx < pa->key[pos]) { --pos; }
	Insert_Brch_Item(pa, pos, kx, s);
	if (pa->num > LEAFMAX)
	{
		return Splice_Brch(pa);
	}
	else
	{
		return nullptr;
	}
}

创建一个分支节点

BNode* BuyBrchnode()
{
	BNode* s = Buynode();
	s->parent = nullptr;
	s->utype = BRCH;
	return s;
}

移动分支节点的值

void Insert_Brch_Item(BNode* ptr, int pos, KeyType kx, BNode* right)
{
	for (int i = ptr->num; i > pos; --i)
	{
		ptr->key[i + 1] = ptr->key[i];
		ptr->sub[i + 1] = ptr->sub[i];
	}
	ptr->key[pos + 1] = kx;
	ptr->sub[pos + 1] = right;	 // right->parent;
	ptr->num += 1;
}
好了,插入代码到此结束了;

测试一下子23,33,12,10,48,50 如图所示,我们去看看编译器;

 测试如图

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/1295893.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-10
下一篇 2022-06-10

发表评论

登录后才能评论

评论列表(0条)

保存