[C++数据结构](34)B树

news2024/11/26 11:41:18

文章目录

  • B 树概念与性质
  • B 树基本操作与实现
    • 框架
    • 查找
    • 插入
    • 遍历
  • B 树性能分析及其优势

B 树概念与性质

1970 年,R.Bayer 和 E.mccreight 提出了一种适用于外查找的树,它是一种平衡的多叉树,称为 B 树(或 B-树、B_树)。

一棵 m m m ( m > 2 ) (m > 2) (m>2) 的 B 树,是一棵 m m m 路平衡搜索树( m m m 表示这个树的每一个结点最多可以拥有的子结点个数),可以是空树或满足以下性质:

  1. 根结点至少有两个孩子
  2. 每个分支结点都包含 k − 1 k-1 k1 个关键字和 k k k 个孩子(孩子的个数永远比关键字多一个),其中 ⌈ m 2 ⌉ ≤ k ≤ m \lceil\frac{m}{2}\rceil\le k\le m 2mkm
  3. 每个叶子结点都包含 k − 1 k-1 k1 个关键字,其中 ⌈ m 2 ⌉ ≤ k ≤ m \lceil\frac{m}{2}\rceil\le k\le m 2mkm
  4. 所有叶子结点都在同一层
  5. 每个结点中的关键字从小到大排列,结点中 k − 1 k-1 k1 个元素正好是 k k k 个孩子包含的元素的值域划分
  6. 每个结点的结构为: ( n , A 0 , K 1 , A 1 , K 2 , A 2 , … , K n , A n ) (n,A_0,K_1,A_1,K_2,A_2,\dots,K_n,A_n) (n,A0,K1,A1,K2,A2,,Kn,An) 其中, K i ( 1 ≤ i ≤ n ) K_i(1\le i\le n) Ki(1in) 为关键字,且 K i < K i + 1 ( 1 ≤ i ≤ n − 1 ) K_i<K_{i+1}(1\le i\le n-1) Ki<Ki+1(1in1) A i ( 0 ≤ i ≤ n ) A_i(0\le i\le n) Ai(0in) 为指向子树根结点的指针。且 A i A_i Ai 所指子树所有结点中的关键字均小于 K i + 1 K_i+1 Ki+1 n n n 为结点中关键字个数,满足 ⌈ m 2 ⌉ − 1 ≤ n ≤ m − 1 \lceil\frac{m}{2}\rceil-1\le n\le m-1 2m1nm1

示例:一个 3 阶 B 树

img

B 树基本操作与实现

框架

结点的结构,包含key数组和subs孩子结点指针数组,这两个数组都要多开一个空间,方便后面先插入再分裂的实现。此外,还需要一个父指针,后面实现插入的时候也用得到。

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;
	}
};

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

private:
	Node* _root = nullptr;
};

查找

按照搜索树的查找规则进行查找即可,与我们之前写的搜索树不同的是,B 树的每个结点可能有多个 key,所以对每个结点内的 key 都要逐个比较。

	// 查找,返回关键字所在结点指针及下标
	// 未找到,则返回该关键字应该插入在哪个叶子结点,返回下标为-1
	pair<Node*, int> Find(const K& key)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		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 { cur, i };
			}
			parent = cur;
			cur = cur->_subs[i];
		}
		return { parent, -1 };
	}

插入

针对一棵高度为 h h h m m m 阶 B 树,插入一个元素时,首先要验证该元素在 B 树中是否已经存在,如果不存在,那么就要在叶子结点中插入该新元素。步骤如下:(注:一个结点最多允许拥有 m − 1 m-1 m1 个关键字,但我们往往会多开一个空间,允许拥有 m m m 个关键字,并将有 m m m 个关键字称为”满了“)。

  1. 按照搜索树规则将元素插入到叶子结点
  2. 如果叶子结点空间满了,即该结点的关键字个数有 m m m 个,则需要将该结点进行分裂,将一半数量的关键字分裂到新的与其相邻的右兄弟结点中,中间关键字上移到父结点。
    1. 从该结点中选出中位数
    2. 小于这一中位数的元素留在左结点,大于这一中位数的放入右结点,中位数作为分隔值
    3. 分隔值被插入到父结点中,这可能导致父结点满了,进行分裂,分裂步骤同上。父结点分裂又可能导致它的父结点分裂,以此类推,直到父结点无须分裂为止。如果没有父结点(即遇到根结点),就创建一个新的根结点(增加一层)。

示意图

以 3 阶 B 树为例 ( m = 3 ) (m=3) (m=3)

img

向一个结点里插入 key 和 孩子:

运用插入排序的思想,把 key 和其对应的右孩子同时往后挪,找到合适的位置把 新key新孩子 插入。不要忘记把新孩子的父指针指向 node 父亲。

	void InsertKey(Node* node, const K& key, Node* child)
	{
		int end = node->_n - 1;
		while (end >= 0)
		{
			if (key < node->_keys[end])
			{
				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;
		}

		// 查找
		pair<Node*, int> ret = Find(key);
		// 已存在
		if (ret.second >= 0)
			return false;

		// 不存在,插入新元素
		Node* parent = ret.first;
		K newKey = key;
		Node* child = nullptr;

		while (1)
		{
			InsertKey(parent, newKey, child);
			if (parent->_n < M) // 无须分裂
				return true;
			// 分裂
			size_t mid = M / 2;
			// 分出兄弟
			Node* brother = new Node;
			size_t j = 0;
			for (size_t i = mid + 1; i < M; ++i)
			{
				brother->_keys[j] = parent->_keys[i];
				brother->_subs[j++] = parent->_subs[i];
				if (parent->_subs[i])
					parent->_subs[i]->_parent = brother;
			}
			brother->_subs[j] = parent->_subs[M];
			if (parent->_subs[M])
				parent->_subs[M]->_parent = brother;
			brother->_n = j;
			parent->_n -= j + 1;
			brother->_parent = parent->_parent;

			// 把中位数分给根
			if (parent->_parent == nullptr) // 没有根,则创建一个新的根,结束
			{
				_root = new Node;
				_root->_keys[0] = parent->_keys[mid];
				_root->_subs[0] = parent;
				_root->_subs[1] = brother;
				_root->_n = 1;
				parent->_parent = _root;
				brother->_parent = _root;
				break;
			}
			// 有根,转换,准备循环
			newKey = parent->_keys[mid];
			child = brother;
			parent = parent->_parent;
		}
		return true;
	}

尤其要注意分裂分给兄弟的部分,不仅要将 key 和 孩子指针 分出去,还有注意分出去的孩子的父指针也应该改变。最后也不要忘了将兄弟结点的父指针指向对应的父结点。

遍历

	void _InOrder(Node* cur)
	{
		if (cur == nullptr)
			return;
		for (size_t i = 0; i < cur->_n; ++i)
		{
			_InOrder(cur->_subs[i]);
			cout << cur->_keys[i] << " ";
		}
		_InOrder(cur->_subs[cur->_n]);
	}

	void InOrder()
	{
		_InOrder(_root);
	}

B 树性能分析及其优势

B 树的优势就在于一个结点可以存储多个数据,相比二叉搜索树而言,B 树可以将树的高度进一步压缩。并且能够将逻辑上相连的数据存储在一起。

考虑在磁盘中存储数据的情况,与内存相比,读写磁盘有以下不同点:

  1. 读写磁盘的速度相比内存读写慢很多。
  2. 每次读写磁盘的单位要比读写内存的最小单位大很多。

由于读写磁盘的这个特点,因此对应的数据结构应该尽量的满足 “局部性原理”:“当一个数据被用到时,其附近的数据也通常会马上被使用”,为了满足局部性原理, 所以应该将逻辑上相邻的数据在物理上也尽量存储在一起。这样才能减少读写磁盘的数量。

所以,对比起一个节点只能存储一个数据的 BST 类数据结构来,要求这种数据结构在形状上更 “胖”、更加 “扁平”,即:每个节点能容纳更多的数据, 这样就能降低树的高度,同时让逻辑上相邻的数据都能尽量存储在物理上也相邻的硬盘空间上,减少磁盘读写。


B 树上大部分基本操作所需访问盘的次数均取决于树高 h h h.

下面就最坏情况进行分析,

对任意一棵具有 n n n 个关键字的 m m m 阶 B 树 ( n ≥ 1 , m ≥ 3 ) (n\ge1,m\ge3) (n1,m3),设最小度数 t ≥ 2 t\ge2 t2,其根结点包含至少一个关键字,其他结点包含至少 t − 1 t-1 t1 个关键字。

这样,在深度为 1 有 1 1 1 个根结点,在深度为 2 至少有 2 2 2 个结点,在深度 3 至少有 2 t 2t 2t 个结点,在深度 4 至少有 2 t 2 2t^2 2t2 个结点,在深度 h 至少有 2 t h − 2 2t^{h-2} 2th2 个结点。

由此,关键字的个数 n n n 满足在不等式:
n ≥ 1 + ( t − 1 ) ∑ i = 0 h − 2 2 t i = 1 + 2 ( t − 1 ) 1 − t h − 1 1 − t = 2 t h − 1 − 1 n\ge1+(t-1)\sum_{i=0}^{h-2}2t^{i}=1+2(t-1)\frac{1-t^{h-1}}{1-t}=2t^{h-1}-1 n1+(t1)i=0h22ti=1+2(t1)1t1th1=2th11

h ≤ log ⁡ t n + 1 2 + 1 h\le\log_t\frac{n+1}{2}+1 hlogt2n+1+1
⌈ m 2 ⌉ ≤ t ≤ m \lceil\frac m2\rceil\le t\le m 2mtm,所以 B 树的高度可以记为 O ( log ⁡ m n ) O(\log_mn) O(logmn),于是在 B 树上查找、插入和删除的读写盘的时间复杂度为 O ( log ⁡ m n ) O(\log_mn) O(logmn)

所以 B 树的阶数 m 我们往往设计得很大,以降低基本操作的时间复杂度。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数字先锋 | 主机、硬盘、CPU统统没有? 这个电教室有点“潮”!

百年大计&#xff0c;教育为本。在数字中国建设整体布局和推进下&#xff0c;教育新基建为教育高质量发展构筑了数字底座&#xff0c;更为教育创新变革提供了强大的技术基础和应用环境。近年来&#xff0c;教育部门紧抓新一代信息技术发展机遇&#xff0c;赋能学校高质量发展&a…

使用YOLOV5训练口罩检测模型

一、YOLOV5源码和口罩数据集的下载与导入 我是在github上下载的&#xff0c;地址是GitHub - ultralytics/yolov5 at v7.0&#xff0c;下载之后&#xff0c;我将其导入pycharm环境中&#xff0c; 数据集在roboflow上面下载的&#xff0c;地址是Computer Vision Datasets&#…

Linux基础-压缩与打包

该文章主要为完成实训任务&#xff0c;详细实现过程及结果见【参考文章】 参考文章&#xff1a;https://howard2005.blog.csdn.net/article/details/127129580 文章目录一、压缩与打包1.1 压缩1.2 打包1.3 压缩或打包文件常见拓展名二、gzip命令2.1 命令作用2.2 选项参数2.3 实…

python中pandas进行数据分析与可视化(1)

1.创建数据源 matplotlib库是用于数据可视化&#xff0c;pandas则是用于数据分析&#xff0c;在导入这两个包之后&#xff0c;我们可以手工创建两个数据&#xff0c;其表示的是在某一年里&#xff0c;不同名称的新生儿数量&#xff0c;比如被叫做Bob的新生儿有968位 import m…

基于模型预测算法的混合储能微电网双层能量管理系统研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

动态规划之01背包问题

背包练习网址https://www.luogu.com.cn/contest/92872 想要做题的话可以到这里面来进行完成&#xff08;邀请码&#xff1a;r36l&#xff09;。注&#xff1a;要输入邀请码才可以进入。 满篇都是干货&#xff0c;有详细的注释和代码&#xff0c;请放心观看。 这就是传说中的 0…

[附源码]Python计算机毕业设计二手交易平台管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

数据生态第四弹 | OpenMLDB Hive Connector,架构起数据仓库到特征工程的生态桥梁

导读 近日&#xff0c;OpenMLDB 实现了与开源数据仓库软件 Hive 的连接&#xff0c;继完成与 Kafka、Pulsar、RocketMQ 等实时数据源生态整合后&#xff0c;持续构建离线数据生态&#xff0c;期待建设一个更加全面一体的上下游生态圈&#xff0c;在吸引更多用户的同时也能降低…

【能效管理】AcrelEMS-UT综合管廊能效管理平台解决方案应用分析

平台概述 AcrelEMS-UT综合管廊能效管理平台集电力监控、能源管理、电气安全、照明控制、环境监测于一体&#xff0c;为建立可靠、安全、高效的综合管廊管理体系提供数据支持&#xff0c;从数据采集、通信网络、系统架构、联动控制和综合数据服务等方面的设计&#xff0c;解决了…

DC-4靶场教程

探测目标靶机&#xff0c;获取到ip arp-scan -l扫描开放的端口 masscan -p1-65535 192.168.250.213 --rate10000使用nmap获取端口、服务版本、系统等详细信息 nmap -sC -sV -p- -A -T4 192.168.250.213查看使用的中间件 whatweb http://192.168.250.213/访问80端口 没有其…

NLP 模型“解语如神”的诀窍:在文本分类模型中注入外部词典

一. 引言 现实世界的文本表述如恒河沙数&#xff0c;以惊人的速度变换着&#xff0c;人工智能&#xff08;AI&#xff09;在快速识别形形色色的文本之前&#xff0c;必须经过充足的训练数据洗礼。然而&#xff0c;面对复杂多变的文本表述&#xff0c;NLP 模型往往无法从有限的…

架构设计(一):从单服务器模式到负载均衡设计

架构设计&#xff08;一&#xff09;&#xff1a;从单服务器模式到负载均衡设计 作者&#xff1a;Grey 原文地址&#xff1a; 博客园&#xff1a;架构设计&#xff08;一&#xff09;&#xff1a;从单服务器模式到负载均衡设计 CSDN&#xff1a;架构设计&#xff08;一&…

latex设置citation显示作者+年份

如果是bib文件分开放&#xff0c;并且每个引用都明确写了author和year&#xff0c;那么直接\citep 就可以&#xff0c;就能产生(abc et al., 2015) 这种格式, 如果你不想要圆括号&#xff0c;可以使用\usepackage[square]{natbib}, 也可以使用\setcitestyle{authoryear,open{(}…

java计算机毕业设计基于安卓Android的论坛App

项目介绍 首先,论文一开始便是清楚的论述了系统的研究内容。其次,剖析系统需求分析,弄明白“做什么”,分析包括业务分析和业务流程的分析以及用例分析,更进一步明确系统的需求。然后在明白了系统的需求基础上需要进一步地设计系统,主要包罗软件架构模式、整体功能模块、数据库设…

【Kubernetes】Pod数据存储

kubernetes&#xff0c;是一个全新的基于容器技术的分布式架构领先方案&#xff0c;是谷歌严格保密十几年的秘密武器----Borg系统的一个开源版本&#xff0c;于2014年9月发布第一个版本&#xff0c;2015年7月发布第一个正式版本。 kubernetes的本质是一组服务器集群&#xff0…

时间序列异常检测(adtk)

1. 获取时间序列数据 未安装adtk的先安装&#xff1a;pip install adtk 2.阈值检测 adtk.detector.ThresholdAD(lowNone, highNone) 参数&#xff1a; low&#xff1a;下限&#xff0c;小于此值&#xff0c;视为异常 high&#xff1a;上限&#xff0c;大于此值&#xff0c;视为…

基于java+springboot+mybatis+vue+mysql的4S店车辆管理系统

项目介绍 任何系统都要遵循系统设计的基本流程&#xff0c;本系统也不例外&#xff0c;同样需要经过市场调研&#xff0c;需求分析&#xff0c;概要设计&#xff0c;详细设计&#xff0c;编码&#xff0c;测试这些步骤&#xff0c;基于java语言设计并实现了4S店车辆系统&#…

回溯3-----BFS(一石激起千层浪)

文章目录广度优先搜索力扣429-----N叉树的层序遍历力扣994-----腐烂的橘子力扣127 -------单词接龙力扣725------打开转盘锁广度优先搜索 需要借助 队列 来解决问题 例如二叉树的层序遍历 &#xff1a; 1&#xff0c; 将根节点入队 2&#xff0c; 队列出队的时候&#xff0c; …

第一章 OAuth2.0规范(史上最详细解释)——介绍

目录 一、简介 二、角色 二、协议流程 三、授权许可 1、授权码 2、隐式许可 3、资源所有者密码凭据 4、客户端凭据 四、访问令牌 五、刷新令牌 六、TLS版本 七、HTTP重定向 八、互操作性 九、符号约定 一、简介 在传统的客户端-服务器身份验证模式中&#xff0c;客…

docker搭建redis三种集群模式

文章目录一、主从复制1、新建修改配置文件2、执行命令启动redis3、启动客户端测试二、哨兵模式Sentinel1、修改配置文件2、启动sentinel进程3、测试三、集群模式Cluster1、搭建集群1.1、创建集群挂载文件1.2、修改配置文件1.3、docker启动先六个redis实例1.4、构建集群1.5、查看…