探索B-树

1.什么是B-树

B树,是一棵适用于外查找(硬盘查找)的树。 我们常见的查找树有AVL树、红黑树,但是这些都是用于内查找(内存之中),这些树的查找效率为树的高度,即O(lgN)

如果在外查找中,也是用这种结构,每一层都需要访问一次硬盘,而访问硬盘的速度是比较慢的,为了提高外查找的效率,提出了一种平衡的多叉树

由于二叉搜索树,每多一层,就需要多访问一次硬盘,所以B树就是压缩了高度的二叉搜索树

2.B-树的性质

一棵M阶(M>2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足以下性质:

  • 根节点关键字的数量[1,M-1],孩子的数量[2,2M]
  • 每个非根节点至少有(M/2-1)个关键字,至多有M-1个关键字,并且以升序排列(满了就会分裂)
  • 每个非根,非叶子节点至少有M/2个孩子,至多有M个孩子(关键字+1)
  • 一个节点中的关键字按照升序排列,孩子的数量比关键字的个数多1
  • 所有叶子节点都在同一层(横/上 增长)
  • key[i] 和 key[i+1] 之间的孩子节点的值介于key[i]、key[i+1] 之间(二叉树搜索树性质)

3.B-树插入过程

  • 为了保证key的数量,比孩子的数量少一个,新插入的值都是插入到叶子节点之中
  • 当某个节点满了,进行分裂,新创建一个兄弟节点,拷贝右半区间到兄弟节点,中位数提取到放到父亲处,如果没有父亲则创建新的根,分裂出来的两个节点分别做这个中位数的左右孩子
  • 分裂规则中,新节点永远是横向和向上增长,所以叶子节点永远在同一层

假设M=3,需要插4,5,6,7,8,10,36,16,4

在这里插入图片描述

4.B-树的效率

  • 对于一棵节点为N,度为M的B-树,查找和插入需要log(m-1)N~log(m/2)N 次比较,这个很好证明:对于度为M的B-树,每一个节点的子节点个数为(M/2-1) ~(M-1)之间,因此树的高度应该在要log(m-1)N 和log(m/2)N 之间,在定位到该节点后,再采用二分查找的方式可以很快的定位到该元素。

    等比为 q= m/2-1 ~m-1
    Sn = a(1-q^h)/1-q = N
    => h = logq N =>  log(m-1) N ~ log(M/2)N 
    
  • B-树的效率是很高的,对于N = 62*1000000000个节点,如果度M为1024,则 <= 4,即在620亿个元素中,如果这棵树的度为1024,则需要小于4次即可定位到该节点,然后利用二分查找可以快速定位到该元素,大大减少了读取磁盘的次数。

  • B树的核心是分裂,分裂保证树的平衡和节点空间利用至少接近50%

    image-20210717102249114

6.B-树是怎么做到减少与内存交互的次数的

  • 我们的外存,比如硬盘,是将所有的信息分割成相等大小的页面,每次硬盘读写的都是一个或多个完整的页面,对于一个硬盘来说,一页的长度可能是211到214个字节。
  • 在一个典型的B树应用中,要处理的硬盘数据量很大,因此无法一次全部装入内存。因此我们会对B树进行调整,使得B树的阶数(或结点的元素)与硬盘存储的页面大小相匹配。
  • 比如说一棵B树的阶为1001(即1个结点包含1000个关键字),高度为2,它可以储存超过10亿个关键字,我们只要让根结点(数据的地址)持久地保留在内存中,那么在这棵树上,寻找某一个关键字至多需要两次硬盘的读取即可
  • 这就好比我们普通人数钱都是一张一张的数,而银行职员数钱则是五张、十张,甚至几十张一数,速度当然是比常人快了不少。通过这种方式,在有限内存的情况下,每一次磁盘的访问我们都可以获得最大数量的数据(放在同一层)。由于B树每结点可以具有比二叉树多得多的元素,所以与二叉树的操作不同,它们减少了必须访问结点和数据块的数量,从而提高了性能。可以说,B树的数据结构就是为内外存的数据交互准备的。
  • B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点;

7.代码实现

在这里插入图片描述

template<class K,class V, size_t M>//K/V值,和阶数M

struct BTreeNode //定义节点
{
	//孩子数量比关键字多1
	//pait<K, V> _kvl[M - 1];//关键字
	//BTreeNode<K, V, M> *_child[M]; //孩子

	//多给一个空间,可以先插入,再分裂
	pair<K, V> _kvl[M];//关键字
	BTreeNode<K, V, M> *_child[M+1]; //孩子数组

	BTreeNode<K, V, M> *_parent;//父指针
	size_t _kvsize;//记录存储了多少个关键字

	BTreeNode()
		:_parent(nullptr)
		, _kvsize (0)
	{
		for (int i = 0; i < M + 1; i++)
		{
			_child[i] = nullptr;//创建一个新节点,孩子节点全部置为空
		}
	}
};

template<class K, class  V,  size_t M>
class BTree
{
	typedef BTreeNode<K, V, M> Node;

public:

	//第i个key的左孩子是child[i]
	//右孩子是child[i+1]
	pair<Node*, int> Find(const K& key)
	{
		Node *parent = nullptr;
		Node *cur = _root; //从根节点开始搜索
		while (cur)
		{
			size_t i = 0;
			//寻找当前节点当中的关键字
			while (i < cur->_kvsize) //如果M比较大,换成二分查找
			{
				if (cur->_kvl[i].first < key)//key大于当前位置,往右边找
					i++;
				else if (cur->_kvl[i].first>key)//key小于当前位置,往左孩子找
					break;
				else
					return make_pair(cur, i);//找到了,返回即可
			}
			//来到这里,要么当前节点的所有关键字都小于key,要么当前节点中有大于key的关键字
			//即需要继续往孩子节点中进行搜索

			parent = cur;//往下一层走之前,记录父亲
			//假设key小于所有的关键字,那么去右孩子中进行寻找,此时i++已经来到下一位置 -> child[i]
			//假设key大于某个关键字,那么需要去该关键字的左孩子寻找 ,此时直接退出循环,因此正是i位置-> child[i]
			cur = cur->_child[i];
		}
		//没有找到
		return make_pair(parent, -1);//返父亲指针
	}


	//在cur节点之中,插入kv,和孩子节点
	void InsertKv(Node* cur,const pair<K, V> &kv,Node* brother)
	{
		//从后往前寻找
		size_t i = cur->_kvsize;
		for (i; i > 0; i--)
		{
			if (kv.first > cur->_kvl[i-1].first)//插入的关键字,大于前面的关键字,说明是插在此位置
			{
				break;
			}
			//插入关键字小于前面的位置,则将前面的位置往后挪,同时孩子节点也需要挪动
			cur->_kvl[i] = cur->_kvl[i - 1];
			cur->_child[i+1] = cur->_child[i];//孩子的下标比关键字大1
		}
		//填入关键字,和孩子节点
		cur->_kvl[i] = kv;
		cur->_child[i+1] = brother;
		cur->_kvsize++;//关键字数量自增
	}

	//插入一个关键字
	//先找到对应的位置,然后插入
	//如果当前key值已经存在则不能插入
	//key不存在,插入到叶子节点之中,因此在插入的时候,需要找到叶子节点
	bool Insert(const pair<K, V> &kv)
	{
		if (_root == nullptr)//根节点为空
		{
			_root = new Node;
			_root->_kvl[0] = kv;//填入当前关键字
			_root->_kvsize = 1;//关键字++
			return true;
		}

		//根不为空
		//根不为空时,先查找,如果找到了,则不被允许插入
		pair<Node*, int> ret = Find(kv.first);

		if (ret.second >= 0)//表示当前已经存在,不可插入
		{
			return false;
		}

		//没有找到,可以进行插入
		//此时的ret.first表示可以插入的节点

		Node* cur = ret.first;//获取插入的位置
		//在cur中,插入值kv,此时不需要填入兄弟节点
		InsertKv(cur, kv,nullptr);

		//有可能需要进行分裂处理
		while (1)
		{
			if (cur->_kvsize < M)//当前节点没有满,直接返回成功即可
			{
				return true;
			}
			else//当前节点满了
			{
				//分裂出一个兄弟节点
				Node *brother = new Node;

				//[0,mid-1]位置的关键字留给原来的节点
				//[mid+1,M-1]位置的关键字拷贝给兄弟节点
				int mid = cur->_kvsize / 2;

				//拷贝一半的数据给兄弟节点
				size_t sub = 0;
				for (int i = mid + 1; i < cur->_kvsize; i++)
				{
					brother->_kvl[sub++] = cur->_kvl[i];//拷贝关键字
					brother->_kvsize++;//关键字总数++
				}

				//在向上分裂的过程之中,如果有孩子节点,则需要拷贝孩子节点
				//原来的保留[0,mid]的孩子
				//兄弟节点拷贝[mid+1,size]的孩子
				sub = 0;
				for (int i = mid + 1; i <= cur->_kvsize; i++)
				{
					if (cur->_child[i] == nullptr)//没有孩子了
						break;

					brother->_child[sub++] = cur->_child[i];
					cur->_child[i]->_parent = brother;//更新父节点
					cur->_child[i] = nullptr;
				}

				//原来节点留下的关键字数量=原来的-兄弟中的-交付给根节点的
				cur->_kvsize = cur->_kvsize - brother->_kvsize - 1;

				//将mid处位置的值,放入父节点之中
				//1.如果没有父节点,则需要创建根节点
				//2.如果有父节点,往父节点中插入mid位置的关键字

				if (cur->_parent == nullptr)//没有父节点
				{
					//创建父节点,并且将关键字填入
					_root = new Node;
					_root->_kvl[0] = cur->_kvl[mid];
					_root->_kvsize++;

					//填入孩子节点
					_root->_child[0] = cur;
					_root->_child[1] = brother;

					//填入父节点指针
					cur->_parent = _root;
					brother->_parent = _root;

					return true;
				}
				else//有父亲节点,再次调用插入即可
				{
					pair<K, V> kv = cur->_kvl[mid];//获取需要插入的值
					cur = cur->_parent;//cur变为父节点
					InsertKv(cur, kv,brother);//往父节点中插入kv和孩子节点(当前的brother节点)
					brother->_parent = cur;//填入父指针
				}
			}
		}
	}

	//中序遍历,输出的数是升序的
	void _Inorder(Node *root)
	{
		if (root == nullptr)
			return;

		size_t i = 0;
		while (i < root->_kvsize)
		{
			_Inorder(root->_child[i]);//先输出左边
			cout << root->_kvl[i].first << " ";//再输出当前位置
			i++;
		}
		_Inorder(root->_child[i]);//输出右边
	}

	void Inorder()
	{
		_Inorder(_root);
	}

private:
	Node *_root = nullptr;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值