B+树插入

B+树插入

B+树定义

B+树是一种多路平衡查找树,多用于文件系统中当作索引。对于B+树网上也有好多不同的定义,在使用时大家可以根据实际需求做些许改动,我就采用如下方式:

  1. 树中每个结点最多有M棵子树,同时最多只有M-1个关键字。
  2. 若根节点不是叶子节点则至少有两棵子树。
  3. 除根节点外,其它所有非叶子节点至少有⌈M/2⌉棵子树。
  4. 所有节点上的关键字均已排序(假设为递增序列),且关键字左边子树的值均小于该关键字的值,而右边子树的值大于等于其值。
  5. 除本层最后一个关键字外,其它关键字必须有左、右两个孩子节点。
  6. 所有叶子节点都在同一层上,且包含所有信息。
  7. 非叶子节点上的关键字只充当索引并不代表任何信息。

插入操作

B+树的插入操作主要分几类讨论,并不是太复杂,通过一个示例很容易理解这个过程。我们就依次插入(24, 29, 2, 17,5, 19,3, 18, 7, 11, 37,15,23, 31)这些数据,一步步构建一棵B+树。

  1. 一开始只有一个根节点直接插入数据即可:
    在这里插入图片描述
  2. 接着插入29和2,只需注意排序就行:
    在这里插入图片描述
  3. 当插入17时,会发现当前节点已满,此时需要将该节点分列成两个节点,并再生成一个节点做为新的根节点:在这里插入图片描述
  4. 插入5就是直接往2,17对应节点放就行。但当要插入19时,发现该节点满了,就采用与上面同样的操作进行分裂,并把中间元素17调至根节点:在这里插入图片描述
  5. 按同样的方法插入3,18,7,11,37得到:在这里插入图片描述
  6. 插入15是比较麻烦的一步,也容易出错。通过查找能够知道15应该插入到第二个叶子节点,此时该节点已有3个关键字,则按照之前第3步插入17一样进行分裂,然后把11提到父节点中。但这时我们会发现其父节点也已经满了,那么就需要进行再一次分裂,把17提到新的根节点去,但是要注意在非叶子节点分裂时,向上提的数17不能在当前层节点中出现(这一步是最容易出错的),因为不这样就无法满足定义中的4和5:在这里插入图片描述
  7. 最后的23和31的插入就简单了,就不再累述:在这里插入图片描述

实现

下面给出C++的代码

template <typename T, int M = 4>		//M为阶数
class BPTree
{
private:
	BPNode* root;		//根节点
	BPNode* firstLeaf;		//第一个叶子节点
	BPNode* lastLeaf;		//最后一个叶子节点
public:
	struct BPNode
	{
		T keys[M];		//关键字
		BPNode* nodes[M + 1];		//孩子节点指针
		BPNode* pre, *next, *parent;		//节点中包含父节点指针,且叶子节点用双链表连接
		int nKey;		//关键字个数,关键字和孩子节点数组都多开辟了一个空间方便插入

		BPNode()
		{
			nKey = 0;
			pre = next = parent = 0;
			for (int i = 0; i < M + 1; i++)
				nodes[i] = 0;
		}
	};

public:
	BPTree()
	{
		root = new BPNode;
		firstLeaf = lastLeaf = root;

	}

	bool IsEmpty()
	{
		return root->nKey == 0;
	}
	
	void Insert(const T& x)
	{
		if (IsEmpty())		//插入第一个数
		{
			root->keys[0] = x;
			root->nKey++;
			return;
		}
		BPNode* p = root;
		while (p->nodes[0])		//寻找插入数据的节点
		{
			int i;
			for (i = 0; i < p->nKey; i++)
			{
				if (x < p->keys[i])
					break;
				if (x == p->keys[i])
					return;
			}
			p = p->nodes[i];
		}
		__Insert(p, x);		//因为一开始多开辟了空间就不管节点是否满直接插入数据
		if (p->nKey < M)		//若插入后节点未溢出就完成插入,否则进行分裂
			return;
		BPNode *left, *right;
		T midKey;
		__SplitNode(p, left, right, midKey, true);		//将p分裂成两个节点
		//将叶子节点用双链表连接起来
		right->next = left->next;
		left->next = right;
		right->pre = left;
		if (right->next)
			right->next->pre = right;
		else
			lastLeaf = right;
		BPNode *pp = p->parent;
		int pos = __Insert(pp, midKey);		//midKey是要向上提的元素,将其插入到父节点
		left->parent = pp;
		right->parent = pp;
		pp->nodes[pos] = left;
		pp->nodes[pos + 1] = right;
		__AdjustParent(pp);		//父节点中插入midKey后还需要对父节点进行调整
	}

private:
	int __Insert(BPNode*& p, const T& x)
	{
		if (!p)		//第一次插入或根节点分裂产生新的根节点
		{
			p = new BPNode;
			root = p;
		}
		int pos = 0; 
		while (pos < p->nKey && x >= p->keys[pos])		//寻找插入位置
		{
			if (x == p->keys[pos])
				return -1;
			pos++;
		}
		for (int j = p->nKey; j > pos; j--)	//将插入位置后面的元素向后移动
		{
			p->keys[j] = p->keys[j - 1];
			p->nodes[j + 1] = p->nodes[j];
		}
		//插入数据
		p->keys[pos] = x;
		p->nKey++;
		return pos;
	}

	//把节点p分裂成left和right两个节点,midKey用来返回上提的元素,containMid表示右节点中是否需要包含上提元素
	void __SplitNode(BPNode* p, BPNode*& left, BPNode*& right, T& midKey, bool containMid)
	{
		int mid = p->nKey / 2;
		midKey = p->keys[mid];
		right = new BPNode;
		left = p;
		int i, j;
		for (i = mid, j = 0; i < p->nKey; i++)		//mid以后的节点复制到right节点中
		{
			if (!containMid && i == mid)
				continue;
			right->keys[j] = p->keys[i];
			right->nodes[j] = p->nodes[i];
			right->nKey++;
			j++;
		}
		right->nodes[j] = p->nodes[i];
		left->nKey = mid;		//左节点中只修改nKey的值不需要真正删除元素
	}

	void __AdjustParent(BPNode* p)
	{
		if (!p || p->nKey < M)		//p节点中未溢出则直接完成插入
			return;
		BPNode *left, *right;
		T midKey;
		__SplitNode(p, left, right, midKey, false);
		BPNode* pp = p->parent;
		int pos = __Insert(pp, midKey);
		pp->nodes[pos] = left;
		pp->nodes[pos + 1] = right;
		//分裂后注意修改right节点的孩子节点的父指针
		for (int i = 0; i <= right->nKey; i++)		
			right->nodes[i]->parent = right;
		left->parent = right->parent = pp;
		__AdjustParent(pp);	//上提元素后,继续递归调整父节点直到没有溢出为止
	}
};

总结

这是第一次接触较复杂的数据结构,由于能力有限,感觉完成插入代码还是挺费劲的,如有错误希望大家指正,谢谢。

作者: 只短浅长

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值