数据库学习之B-树

常见的搜索结构–引入

内查找:
在这里插入图片描述

外查找:B树系列
内外区分:内:内存

如果数据量很大,比如有100G数据,无法一次放进内存中,那就只能放在磁盘上了,如果放在磁盘上,有需要搜索某些数据,那么如何处理呢?那么我们可以考虑将存放关键字及其映射的数据的地址放到一个内存中的搜索树的节点中,那么要访问数据时,先取这个地址去磁盘访问数据。
磁盘中的数据是挨着存储的,不方便搜索。

使用平衡二叉树搜索树的缺陷
平衡二叉树搜索树的高度是logN,这个查找次数在内存中是很快的。但是当数据都在磁盘中时,访问磁盘速度很慢,在数据量很大时,logN次的磁盘访问,是一个难以接受的结果。
使用哈希表的缺陷
哈希表的效率很高是O(1),但是一些极端场景下某个位置冲突很多,导致访问次数剧增,也是难以接受的。
那如何加速对数据的访问呢?

  1. 提高IO的速度(SSD相比传统机械硬盘快了不少,但是还是没有得到本质性的提升)
  2. 降低树的高度—多叉树平衡树

B树概念

平衡搜索树基础上找优化空间:
1.压缩高度,二叉变多叉
2.一个节点里面有多个关键字及映射的值

1970年,R.Bayer和E.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树,称为B树(后面有一个B的改进版本B+树,然后有些地方的B树写的的是B-树,注意不要误读成"B减树")。一棵m阶(m>2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足一下性质:

  1. 根节点至少有两个孩子
  2. 每个分支节点都包含k-1个关键字和k个孩子,其中 ceil(m/2) ≤ k ≤ m ceil是向上取整函数
  3. 每个叶子节点都包含k-1个关键字,其中 ceil(m/2) ≤ k ≤ m
  4. 所有的叶子节点都在同一层
  5. 每个节点中的关键字从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域划分
  6. 每个结点的结构为:(n,A0,K1,A1,K2,A2,… ,Kn,An)其中,Ki(1≤i≤n)为关键字,且Ki<Ki+1(1≤i≤n-1)。Ai(0≤i≤n)为指向子树根结点的指针。且Ai所指子树所有结点中的关键字均小于Ki+1。
    n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。
  7. 每个节点关键字的数量比孩子少一个

插入图解

在这里插入图片描述

总结一些点:
1.每次插入的位置最开始一定是叶子节点,然后根据规则进行调整

关于删除

节点数量小于m/2-1,则优先找父亲借,父亲找兄弟借
找父亲兄各地借不到节点了,再借他们也不满足条件了m/2-1,合并兄弟节点

关于遍历

中序是因为是顺序的
顺序是:
在这里插入图片描述

代码实现:

#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<string>

template<class K, size_t M>
struct BTreeNode
{
	//K _keys[M - 1];
	//BTreeNode<K, M>* _subs[M];

	//为了方便插入以后再分裂,多加一个空间
	K _keys[M];
	BTreeNode<K, M>* _subs[M + 1];
	BTreeNode<K, M>* _parent;
	size_t _n;//记录实际上有多少个关键字

	BTreeNode()
	{
		for (size_t i = 0; i < M; ++i)
		{
			_keys[i] = K();
			_subs[i] = nullptr;
		}

		_subs[M] = nullptr;
		_parent = nullptr;
		_n = 0;
	}
};


//如果数据存在磁盘,K是磁盘地址
template<class K,size_t M>
class BTree
{
	typedef BTreeNode<K, M> Node;
public:

	pair<Node* , int> Find(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		size_t i = 0;
		while (cur)
		{
			//在一个节点内找
			while (i < cur->_n)
			{
				if (key < cur->_keys[i])
				{
					break;
				}
				else if (key > cur->_keys[i])
				{
					++i;
				}
				else
				{
					return make_pair(cur, i);
				}
			}
			parent = cur;
			cur = cur->_subs[i];
		}

		return make_pair(parent,-1);
	}

	void InsertKey(Node* node,const K& key,Node* child)
	{
		int end = node->_n - 1;
		while (end >= 0)
		{
			if (node->_keys[end] > key)
			{
				node->_keys[end + 1] = node->_keys[end];
				node->_subs[end + 2] = node->_subs[end + 1];
				--end;
			}
			else
			{
				break;
			}
		}
		node->_keys[end + 1] = key;
		node->_subs[end + 2] = child;
		if (child)
		{
			child->_parent = node;
		}
		node->_n++;
	}
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node;
			_root->_keys[0] = key;
			_root->_n++;

			return true;
		}

		// key已经存在,不允许插入
		pair<Node*, int> ret = Find(key);
		if (ret.second >= 0)
		{
			return false;
		}

		// 如果没有找到,find顺便带回了要插入的那个叶子节点

		// 循环每次往cur插入 newkey和child
		Node* parent = ret.first;
		K newKey = key;
		Node* child = nullptr;
		while (1)
		{
			InsertKey(parent, newKey, child);
			// 满了就要分裂
			// 没有满,插入就结束
			if (parent->_n < M)
			{
				return true;
			}
			else
			{
				size_t mid = M / 2;
				// 分裂一半[mid+1, M-1]给兄弟
				Node* brother = new Node;
				size_t j = 0;
				size_t i = mid + 1;
				for (; i <= M - 1; ++i)
				{
					// 分裂拷贝key和key的左孩子
					brother->_keys[j] = parent->_keys[i];
					brother->_subs[j] = parent->_subs[i];
					if (parent->_subs[i])
					{
						parent->_subs[i]->_parent = brother;
					}
					++j;

					// 拷走重置一下方便观察
					parent->_keys[i] = K();
					parent->_subs[i] = nullptr;
				}

				// 还有最后一个右孩子拷给
				brother->_subs[j] = parent->_subs[i];
				if (parent->_subs[i])
				{
					parent->_subs[i]->_parent = brother;
				}
				parent->_subs[i] = nullptr;

				brother->_n = j;
				parent->_n -= (brother->_n + 1);

				K midKey = parent->_keys[mid];
				parent->_keys[mid] = K();


				// 说明刚刚分裂是根节点
				if (parent->_parent == nullptr)
				{
					_root = new Node;
					_root->_keys[0] = midKey;
					_root->_subs[0] = parent;
					_root->_subs[1] = brother;
					_root->_n = 1;

					parent->_parent = _root;
					brother->_parent = _root;
					break;
				}
				else
				{
					// 转换成往parent->parent 去插入parent->[mid] 和 brother
					newKey = midKey;

					child = brother;
					parent = parent->_parent;
				}
			}
		}
		return true;
	}

	//关于比哪里
	void _InOrder(Node* cur)
	{
		if (cur == nullptr)
			return;

		// 左 根  左 根  ...  右
		size_t i = 0;
		for (; i < cur->_n; ++i)
		{
			_InOrder(cur->_subs[i]); // 左子树
			cout << cur->_keys[i] << " "; // 根
		}

		_InOrder(cur->_subs[i]); // 最后的那个右子树
	}

	void InOrder()
	{
		_InOrder(_root);
	}

private:
	Node* _root = nullptr;
};


void TestBtree()
{
	int a[] = { 53, 139, 75, 49, 145, 36, 101 };
	BTree<int, 3> t;
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.InOrder();
}

B+树和B*树

B+树

B+树是B树的变形,是在B树基础上优化多路平衡搜索树,B+树的规则跟B树基本类似,但是又在B树的基础上做了以下几点改进优化:

  1. 分支节点的子树指针与关键字个数相同
  2. 分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间
  3. 所有叶子节点增加一个链接指针链接在一起
  4. 所有关键字及其映射数据都在叶子节点出现

在这里插入图片描述

B+树的特性
1.所有关键字都出现在叶子节点的链表中,且链表中的节点都是有序的。
2.不可能在分支节点中命中。
3.分支节点相当于是叶子节点的索引,叶子节点才是存储数据的数据层。

插入
在这里插入图片描述

总结:
相比较于B树
简化了B树孩子比关键字多一个的规则,变成相等!
所有的值都在叶子上,方便遍历查找所有值

B*树

B*树是B+树的变形,在B+树的非根和非叶子节点再增加指向兄弟节点的指针。

在这里插入图片描述

B+树的分裂:
当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。

B*树的分裂:
当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字
(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。
所以,B*树分配新结点的概率比B+树要低,空间使用率更高;

总结:

B树:有序数组+平衡多叉树;
B+树:有序数组链表+平衡多叉树;
B*树:一棵更丰满的,空间利用率更高的B+树。

B树系列劣势:
1.空间利用率低,消耗高
2.插入删除数据时,需要分裂和合并节点,必然要移动数据
3.虽然高低更低,但是在内存中而言,和哈希和平衡搜索树还是一个量级
即:B树系列在内存中体现不出优势

内存中搜索三次和三十次差别不是很大
磁盘中凑所三次和三十次差别非常大

树的应用

数据库的引擎

B+做主键索引相比B树的优势:
1.B+树所有值都在叶子节点,遍历方便,方便区间查找。
2.对于没有建立索引的字段,全表扫描的遍历很方便。
3.分支节点只存储key,一个分支节点空间占用更小,可以尽可能的加载到缓存B树不用叶子就能找到值,B+树一定要到叶子,这是B树的优势,但是B+树高度足够低所以差别不大。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值