数据结构--B树

B树的预备知识 

  我们平常查找比如用搜索二叉树哈希表这些数据结构,一般都是数据在内存中的,这样的话访问数据的速度就很快,这种查找也叫做内查找。二分查找或者直接顺序查找也适合内查找。

  但是当数据量非常大时,比如有100G的数据,此时内存就放不下了,只能放在磁盘上了,在内存外的存储结构上查找也就是外查找。

  对于外查找以我们之前所学习的知识,该用什么思路呢?

  数据在磁盘中,并且是连在一起的,它们也有地址。我们可以用一个平衡搜索树存每个数据的地址,这样每次查找我们需要通过一次文件IO将地址转化成数据进行比较,然后再向下进行。这样的话查找起来主要耗时就在磁盘IO上,如果是这种结构,那么就需要高度次IO,也就是O(logN)级别,比如AVL/红黑树。或者我们也可以用哈希表,它是O(1)级别的,但是哈希表在极端场景下哈希冲突非常严重,也会导致效率低下。

 而且磁盘慢主要是它的每一次查找定位很慢,一旦定位到了就很快了,这跟之后B树设计有关系。

B树就是在搜索树的基础上进行优化的:

1.压缩高度,二叉变多叉

2.一个结点要存多个关键字及映射的值。

B树的概念

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。
第一次接触B树的话看这些概念是非常抽象的,首先我们可以将B树的结点具体化出来

如图,这是m = 3。那么它就最多有m/2个关键字,3个孩子。

不过需要注意的是,一般我们会多开一个空间,以此来方便后续关键字满了后要分裂的场景,比如

m依旧为3,但是结点如下

 在实际设计中,M的值一般会设计的很大,比如1024

 B树的插入

  首先我们知道,一个结点中,关键字的顺序是从小到大排序的,那么当结点中的关键字没有满时,我们就需要找到这个关键字适合插入的位置。

  如果结点满了,那么这个结点就需要分裂出一个兄弟结点,并将自己一半的关键字和孩子分给这个兄弟结点。然后再选一个中位数大小的关键字给父亲结点(也要注意顺序),然后将兄弟结点与父亲结点进行连接,如果父亲结点不存在,那么就先创建一个父亲结点,然后记得连接子结点。

 后续插入就是以此类推,从父结点开始找,根据大小一直找到对应的叶子结点,然后再插入,如果满了就分裂。B树的新结点的插入一定是在叶子结点上的。叶子没有孩子,不影响关键字和孩子的关系,叶子结点满了,就分裂出一个兄弟,再提取中位数,向父亲结点插入这个关键字,和一个孩子(也就是兄弟结点)。

  由此我们可以发现,B树是天然平衡的,因为它的生长是向右和向上生长的。

B树的简单实现

#pragma once
#include <utility>
#include <iostream>

using namespace std;

template<class K,size_t M>
struct BTreeNode
{
	// 为了方便插入以后再分裂,可以多给一个空间
	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模型的而不是K-V模型的
// 外查找时数据存在磁盘里,那么肯定得是K-V模型。
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;

		while (cur)
		{
			// 在一个结点中查找
			size_t i = 0;
			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);  // 到叶子结点了,这个-1是用来判断是否走到叶子结点
	}

	void InsertKey(Node* node, const K& key, Node* child)
	{
		int end = node->_n - 1;
		while (end >= 0)
		{
			if (key < node->_keys[end])
			{
				// 挪动key和它的右孩子
				node->_keys[end + 1] = node->_keys[end];
				node->_subs[end + 2] = node->_subs[end + 1];  // 注意key和它的右孩子的对应关系
				--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)  // 这里就对应了为什么在Node里要多开一个空间。
			{
				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;  // 这里为什么要+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); // 为什么要+1因为还有一个要给父亲

				K midKey = parent->_keys[mid];  // 准备给父亲结点插入key了
				parent->_keys[mid] = K();

				// 说明刚刚分裂是根结点
				if (parent->_parent == nullptr)
				{
					_root = new Node;
					_root->_keys[0] = midKey;
					_root->_subs[0] = parent;
					_root->_subs[1] = brother; // 写到这里再回想一下,B树的规则:根结点至少有两个孩子
					_root->_n = 1;

					parent->_parent = _root;
					brother->_parent = _root;
					break;
				}
				else
				{
					// 如果不是根结点,那么就往父亲结点插入,以此循环,直到新建了一个根结点
					// 或者在下一次插入的父亲结点中,在插入后未满
					newKey = midKey;
					child = brother;
					parent = parent->_parent;
				}
			}
		}

		return true;
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		size_t i = 0;
		for (; i < root->_n; ++i)
		{
			_InOrder(root->_subs[i]);
			cout << root->_keys[i] << " ";
		}
		_InOrder(root->_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树的基础上做了以下几点改进优化:
分支节点的子树指针与关键字个数相同。
分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间。
所有叶子节点增加一个链接指针链接在一起。
所有关键字及其映射数据都在叶子节点出现。

 

  由图我们可以看到,叶子结点是链接在一起的,这样话我们也可以直接遍历叶子结点来找到我们想要的数据。 

  分支节点里面都只存Key,只有叶子节点里面才存Key和Val。

  分支结点根叶子结点有重复的值,分支结点存的是叶子结点的索引。

  父亲结点存的是孩子结点中最小的值作为索引。

所以现在可以做一个小小的总结:

1.B+树简化了B树孩子比关键字多一个的规则,B+树的关键字和孩子数量相等。

2.父亲中存的孩子结点中最小的值作为索引。

B+树的插入

  B+树的插入跟B树相似,但又略有不同,这里就只说思想了。

但是我们知道,B+树的分支结点只存Key,叶子结点既存Key也存Val。因此,在第一次插入的时候,B+树就有两层!一层做根,一层做叶子。我们依旧以M=3为例

 

第一次插入53

 

一直插入直到49开始第一次分裂

 

  这里的处理也比B树简单一些,它是直接将一半的索引给分裂出来的兄弟结点,然后直接将兄弟的最小值给父亲结点作为索引。 

接下来一直插入,直到插入101进行第二次分裂

第二次分裂与第一次逻辑一样。

接下来再补充插入一些结点(150和155),来看看第三次分裂

 

 此时我们发现分裂后,父亲结点满了,那么父亲结点也需要进行分裂

这样B+树就由最初的两层变成了三层。

这就是B+树的插入思想。 

总结B+树的特性:

1. 所有关键字都出现在叶子节点的链表中,且链表中的节点都是有序的。
2. 不可能在分支节点中命中。因为分支节点中存的是Key。
3. 分支节点相当于是叶子节点的索引,叶子节点才是存储数据的数据层。
实际上,B+树的应用比B树要广泛很多。

B*树

  B*树又是在B+树的基础上作出了新的规则。与B+树不同的是,B*树在非根和非叶子结点中也增加了指向兄弟结点的指针。
我们来总结一下B+树的分裂:
  当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增
加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向
兄弟的指针。

再来说说B*树的分裂:

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

 自己和兄弟各自凑出1/3M,给新的兄弟结点。

总结三种树

B 树:有序数组 + 平衡多叉树;
B+ 树:有序数组链表 + 平衡多叉树;
B* 树:一棵更丰满的,空间利用率更高的 B+ 树。
实际应用中还是B+树使用的更为广泛。

 

B-树的应用

 B-树在内查找和外查找中的表现

内查找

  内查找也就是在内存中查找。

B树系列和哈希表和平衡搜索树(AVL树和RB树)的对比:

如果只看树的高度,和搜索效率的话,B树系列确实不错。

但是B树系列也有缺点:

1.空间利用率低,消耗大。我们可以看到很多结点都不一定会存满。

2.插入数据和删除数据时,会分裂和合并结点,那么必然就会挪动数据,带来隐形的消耗。

3.虽然B树系列高度低,但是就在内存中而言,跟哈希和平衡搜索树还是一个量级的。

总结:B树系列在内存中体现不出优势。

外查找

但是,在外查找,也就是磁盘中就不一样了。

在内存中查找3次和30次,差距并不大。

但是在磁盘中查找3次和30次差距就很大了。

因为我们知道IO操作是很消耗时间的。

因此B树系列在外查找中就能体现出优势。

索引

  B-树最常见的应用就是用来做索引。索引通俗的说就是为了方便用户快速找到所寻之物,比如:
书籍目录可以让读者快速找到相关信息,hao123网页导航网站,为了让用户能够快速的找到有价
值的分类网站,本质上就是互联网页面中的索引结构。
  MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构,简单来说:
索引就是数据结构
  当数据量很大时,为了能够方便管理数据,提高数据查询的效率,一般都会选择将数据保存到数
据库,因此数据库不仅仅是帮助用户管理数据,而且数据库系统还维护着满足特定查找算法的数
据结构,这些数据结构以某种方式引用数据,这样就可以在这些数据结构上实现高级查找算法,
该数据结构就是索引。

 

 

其中B-树的一般就是做MyISAM或InnoDB的搜索引擎的。 

一般是B+树用来做磁盘数据索引的。 

比如我们要创建一个学生的信息表,有学生ID,年龄等信息。

  在创建的时候,我们还需要选择一个信息作为主键,也就是将来搜索时的关键字。有些数据库不准用名字作为主键,因为名字可能不唯一,比如Mysql,但是有些数据库可以。 

  比如这里我们拿学生ID作为主键。那么ID就是Key,Val就是这个学生所有的信息的磁盘地址(这个信息一般就是一行)。

  另外分支结点也是需要存在磁盘中的,因为数据量大了的话,内存中也存不下。但是分支结点理应被缓存到cache中。

  另外当我们在查询数据的时候,我们可以根据ID来找,也可以根据姓名来找。但是ID是主键,用ID来查找的效率要比按姓名来查找的效率要高很多。用主键来查找的效率是 O(logN),而用非主键的信息来查找的效率就是 O(N),因为它是通过遍历叶子结点来进行查找的。

   但是如果我们又经常使用姓名去查找的话,我们可以用姓名建立一个索引。

  当姓名也就是name索引创建以后,那么就会以B+树以name作为key再创建一颗树。这样的话效率就高了。 

mysql的存储引擎

MyISAM引擎是MySQL5.5.8版本之前默认的存储引擎,不支持事物,支持全文检索,使用B+Tree
作为索引结构,叶节点的data域存放的是数据记录的地址。
InnoDB存储引擎支持事务,其设计目标主要面向在线事务处理的应用,从MySQL数据库5.5.8版
本开始,InnoDB存储引擎是默认的存储引擎。InnoDB支持B+树索引、全文索引、哈希索引。但
InnoDB使用B+Tree作为索引结构时,具体实现方式却与MyISAM截然不同。
第一个区别是InnoDB的数据文件本身就是索引文件MyISAM索引文件和数据文件是分离的,
索引文件仅保存数据记录的地址。而InnoDB索引,表数据文件本身就是按B+Tree组织的一个索
引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此
InnoDB表数据文件本身就是主索引。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/417596.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

redis启动错误

错误&#xff1a; Creating Server TCP listening socket 127.0.0.1:6379: bind: No error redis-server.exe redis.windows.conf redis-cli.exe shutdown auth "yourpassword"

【kubernetes】关于云原生之k8s集群中pod的容器资源限制和三种探针

目录 一、关于pod容器的资源限制 1.1资源限制的单位 CPU 资源单位 内存 资源单位 二、关于QOS服务质量&#xff08;pod的调度和驱逐有限制&#xff09; 2.1QoS服务质量分类 guaranteed验证 burstable验证 besteffort验证 2.2驱逐顺序 三、关于pod容器的三种探针 3.…

mobile app 安全扫描工具MobSF了解下

可以干啥&#xff1a; static 静态分析 dynamic 动态分析 可以用来渗透了 如何docker安装 docker image 下载地址https://hub.docker.com/r/opensecurity/mobile-security-framework-mobsf/ setup 两行即可 1 docker pull opensecurity/mobile-security-framework-mobsf…

Python:练习:编写一个程序,录入一个美元数量(int),然后显示出增加%5税率后的相应金额。

案例&#xff1a; 编写一个程序&#xff0c;录入一个美元数量&#xff08;int&#xff09;&#xff0c;然后显示出增加%5税率后的相应金额。格式如下所示&#xff1a; Enter an amount:100 With tax added:$105.0 思考&#xff1a; 1、录入一个美元数量&#xff0c;录入&am…

[Flutter]TextButton自定义样式

在Flutter中&#xff0c;TextButton是一个简单的文本按钮&#xff0c;它遵循当前的Theme。如果你想要修改TextButton的样式&#xff0c;可以通过设置其style属性来实现&#xff0c;该属性接收一个ButtonStyle对象。 给TextButton和里面Text添加背景后&#xff0c;效果如下。可…

【软件测试】接口调不通排查分析+常遇面试题总结

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、接口调不通&am…

【MySQL】MySQL数据管理——DDL数据操作语言(数据表)

目录 创建数据表语法列类型字段属性SQL示例创建学生表 查看表和查看表的定义表类型设置表的类型 面试题&#xff1a;MyISAM和InnoDB的区别设置表的字符集删除表语法示例 修改表修改表名语法示例 添加字段语法示例 修改字段语法示例 删除字段语法示例 数据完整性实体完整性域完整…

petalinux烧写image.ub报错

xinlinx SDK烧写petalinux生成的BOOT.BIN和image.ub时&#xff0c;BOOT.BIN烧写正常&#xff0c;image.ub烧写报错如下 Erase Operation failed. INFO: [Xicom 50-44] Elapsed time 0 sec.ERROR: Flash Operation Failed串口助手操作擦除flash如图&#xff1a; 解决方法&am…

JVM内存分配与垃圾收集流程

3.8 实战&#xff1a;内存分配与回收策略 3.8.1 对象优先在Eden分配 大多数情况下&#xff0c;对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时&#xff0c;虚拟机将发起一次Minor GC。 3.8.2 大对象直接进入老年代 HotSpot虚拟机提供了-XX&#xff1a;Prete…

西软云XMS futurehotel/operate XXE漏洞复现

0x01 产品简介 西软云XMS是基于云平台数据中心开发的支持多酒店、多语言、多平台的酒店管理系统。致力于以新一代云架构为国内四,五星级中高端酒店提供灵活、高度整合酒店业务,助力酒店智能转型升级。 0x02 漏洞复现 西软云XMS /XopServerRS/rest/futurehotel/operate接口…

Spring事务模板及afterCommit存在的坑

大家好&#xff0c;我是墨哥&#xff08;隐墨星辰&#xff09;。今天的内容来源于两个线上问题&#xff0c;主要和大家聊聊为什么支付系统中基本只使用事务模板方法&#xff0c;而不使用声明式事务Transaction注解&#xff0c;以及使用afterCommit()出现连接未按预期释放导致的…

Smuggler:一款功能强大的HTTP请求走私和去同步安全测试工具

关于Smuggler Smuggler是一款功能强大的HTTP请求走私和去同步安全测试工具&#xff0c;该工具基于纯Python 3开发&#xff0c;可以帮助广大研究人员针对应用程序的HTTP协议执行安全分析和测试。 工具安装 由于该工具基于Python 3开发&#xff0c;因此我们首先需要在本地设备上…

【学习记录】HC32F460USB——U盘IAP升级app

从头开始&#xff0c;万物从解压开始 直奔猪蹄&#xff0c;找到usb下的工程文件 开始移植 主要移植IAP的boot和fatfs的文件系统&#xff0c;fatfs官网去下载ff15.0版本&#xff0c;目前用这个 放到项目里 添加到工程文件中 改引脚&#xff0c;给USB放电 编译&#xff0c;可以…

GCC如何产生core dump

先决条件 1.安装apport&#xff08;automatically generate crash reports for debugging&#xff09; 2.修改/etc/security/limits.conf文件&#xff0c;使允许core dump&#xff0c;或者用ulimit -c unlimited设置core dump文件的大小为unlimited&#xff08;临时方案&#x…

生成voc格式数据集

数据集存放格式&#xff1a;&#xff08;Annotations文件夹放标注的xml文件&#xff0c;JPEGImages文件夹放标注的图片&#xff09; 运行代码&#xff1a; import os import random import xml.etree.ElementTree as ETimport numpy as npdef get_classes(classes_path):with …

Java中的时间API:Date、Calendar到Java.time的演变

引言 在软件开发中&#xff0c;处理时间和日期是一项基本且不可或缺的任务。无论是日志记录、用户信息管理还是复杂的定时任务&#xff0c;准确地处理时间都显得至关重要。然而&#xff0c;时间的处理并不像它看起来那么简单&#xff0c;尤其是当我们考虑到时区、夏令时等因素…

计算机组成原理-第一/二章 概述和数据的表示和运算【期末复习|考研复习】

文章目录 前言第一章 计算机组成原理 概述及各种码1.1 计算机硬件的基本组成1.1.1 存储器1.1.2 运算器1.1.3 控制器 1.2 计算机的工作过程1.3 计算机的性能指标1.4 各个字长区别与联系 第二章 数据的表示与运算2.1 ASCII码2.2 各种码2.3 浮点数 总结 前言 给大家整理了一下计算…

域控操作七:让某人不执行某策略/单独放行某人

比如我设置的是禁用移动热点&#xff0c;我在这里对某人拒绝&#xff0c;那他就能用移动热点

Qt6内嵌CEF

一、下载CEF CEF下载地址&#xff1a;https://cef-builds.spotifycdn.com/index.html 或https://bitbucket.org/chromiumembedded/cef/src/master/ 选择对应系统的版本&#xff08;本教程选择的是116.0.19&#xff09; CMake下载地址&#xff1a;https://cmake.org/download…

大数据界面:客户又又又要求科技感了,如何破?

如果你问客户想要什么风格&#xff0c;大部分脱口而出科技感&#xff0c;不仅要求静态页&#xff0c;而且还要求动态效果&#xff0c;炫酷动画&#xff0c;贝格前端工场结合多个项目经历&#xff0c;帮助友友们梳理如何让界面科技动感。 一、没有科技感背后的潜台词 客户说大数…