本文的目的是从B树的起源讲起,再到java语言完整的实现,以达到对B树有一个全面的认识,如果你打算学习并实现B树(但是能在有生之年去实现一遍B树的人很少),那么看完本文就应该可以了。如果你想找B树的应用,那本文不适合。

B树的起源

我一直坚信,一个东西或一项技术的出现一定是有原因的,如果我们能找到那个原因,就能像创造者一样思考为什么要这样,为什么那个人不是我?下面开始。

在1970年,Bayer&McCreight发表的论文《ORGANIZATION AND MAINTENANCE OF LARGE ORDERED INDICES 》(大型有序索引的组织和维护)中提出了一种新的数据结构来维护大型索引,这种数据结构在论文中称为B-Tree,看论文的摘要:

ABSTRACT
Organization and maintenance of an index for a dynamic random access file is considered. It is assumed that the index must be kept on some pseudo random access backup store like a disc or a drum. The index organization described allows retrieval, insertion, and deletion of keys in time proportional to logkIlog_kIlogk​I where I is the size of the index and k is a device dependent natural number such that the performance of the scheme becomes near optimal. Storage utilization is at least 50% but generally much higher. The pages of the index are organized in a special data-structure, so-called B-trees. The scheme is analyzed~ performance bounds are obtained, and a near optimal k is computed. Experiments have been performed with indices up to i00,000 keys. An index of size 15,000 (i00,000) can be maintained with an average of 9 (at least 4) transactions per second on an IBM 360/44 with a 2311 disc.

翻译过来:
考虑组织和维护动态随机访问文件的索引, 假设索引必须保存在某些伪随机访问备份存储中,如光盘或鼓。 所描述的索引组织必须在logkIlog_kIlogk​I时间内成比例地检索,插入和删除键,其中III是索引的大小,kkk是依赖于设备的自然数,使得方案的性能变得接近最优。 存储利用率至少为50%,但通常要高得多。 索引的页面组织在一个特殊的数据结构中,即所谓的B树。 分析该方案〜获得性能界限,并计算近似最优k。 已经使用高达100,000个键的索引进行了实验。 在具有2311个光盘的IBM 360/44上,可以维持大小为15,000(i00,000)的索引,平均每秒9次(至少4次)事务。

B树的定义1

还是先看看B树在论文中如何定义的:

hhh:代表树的高度,kkk是个自然数,一个B树要么是空的,要么满足以下条件:
1.所有叶子节点到根节点的路径长度相同,即具有相同的高度;

2.每个非叶子和根节点(即内部节点)至少有k+1k+1k+1个孩子节点,根至少有2个孩子;

3.每个节点最多有2k+12k+12k+1个孩子节点。

4.每个节点内的键都是递增的(后文提到)

看完了这几个定义,你肯定会问:kkk是什么? 好吧,论文在插入一节提到了:

意思就是:这个B树的每个节点实际上代表了一页(Page),这一页可以看成是一个磁盘块的大小,比如1024,那么就可以存储1024个键,那么这个k就等于512,因为一页最大为2k,最小为k个键,k看来可以是自己定义的,根据你的存储硬件来决定。下图是论文中的一页,可看成是个数组:

B树定义2

但是,大家在网上搜索时会发现定义并不唯一,普遍采用了m阶(代表order,中文翻译为阶)来定义:

  1. 每个节点最多有m个孩子。
  2. 每个内部节点(除去叶节点和根节点)至少有⌈m/2⌉(向上取整)孩子。
  3. 如果根不是叶节点,则根至少有两个孩子。
  4. 所有叶子都出现在同一层。
  5. 具有k个孩子的非叶节点包含k-1个键,当然节点内的键也是递增的。

这个版本是Knuth’s 提出的,可以在下面的期刊中找到: The Art Of Computer Programming 第494页,定义为如下:

定义比较

在网上可能会有这样的比较:一个是按度(degree,树的度为最大节点的度,节点的度就是节点的孩子数),也就是上面的定义1,一个是按阶(order),也就是定义2。

其实,稍微对比一下就会发现,这两个定义是同一个东西,他们的关系就是 2k+1=m2k+1=m2k+1=m(因为 ⌈m/2⌉到m⌈m/2⌉ 到 m⌈m/2⌉到m对应着k+1到2k+1k+1 到 2k+1k+1到2k+1,开始定义很明确,所以如果非要比,那么mmm一般是奇数) .当然,也不用纠结,看看下文为啥会有这些改变。

还有个关键点:即便是每个节点没有存满,也需要分配固定大小的容量,所以才有了存储利用率大于50%一说。

这样的话,定义就基本明白了。

为何术语不统一

为什么关于B树的术语在文献上并不统一(内容来自wiki)。

Order如何定义

Bayer&McCreight(1972),Comer(1979)(参考论文:https://dl.acm.org/citation.cfm?doid=356770.356776 )和其他人将B树的Order定义为非根节点中的最小键数。 Folk&Zoellick(1992)指出此术语含糊不清,因为最大键数不明确。Order为3的 B树可能最多包含6个键或最多7个键。 Knuth(1998)通过将Order定义为最大子项数(比最大键数多一个)来避免此问题,也就是我们现在普遍使用的。

叶子(Leaf)如何定义

叶子一词也不一致。 Bayer&McCreight(1972)认为叶子层是最底层的键,但Knuth认为叶子层低于最底层的键,没有孩子也没有指针(Folk&Zoellick 1992)。

实现的多样性

所以B树有许多种实现。在某些设计中,叶子可能包含整个数据记录; 在其他设计中,叶子可能只保存指向数据记录的指针。但这些并不是B树的基础概念。

为简单起见,大多数作者假设有一定数量的键适合节点。基本假设是键大小是固定的,节点大小是固定的。在实践中,可以使用可变长度的键(Folk&Zoellick 1992)。

理解与实现

关于阶定义的实现

关于普遍存在的一种按照阶的不完全的实现方式: https://algs4.cs.princeton.edu/code/edu/princeton/cs/algs4/BTree.java.html
这个算法需要注意2点:

  1. 上面代码里的m必须是偶数的,为什么,可以查看:stackoverflow,回答是这并不绝对,只是考虑到性能方面的影响,所以,我进行了一点点修改,让它可以传入奇数的m,具体代码查看:B树实现代码片
  2. 这颗B树实现简单,但是只能在叶子节点上存储数据,也就是说非叶节点只有key,没有value。

但是这不是本文要讲的方式,本文按照度的定义来讲,ttt代表一个自定义的整数(t>=2t>=2t>=2)表示树的最小度数,一个节点(除根外)的key的范围为(k−1到2k−1k-1 到 2k-1k−1到2k−1),下面具体看。

本文遵从的定义

我们按照《算法导论》第3版第18章中的B树定义来实现:

  1. 所有叶子节点到根节点的路径长度相同,即具有相同的高度;
  2. 每个非叶子和非根节点(即内部节点)至少有t-1个孩子节点;根至少2个孩子
  3. 每个节点最多有2t个孩子节点。
  4. 每个节点内的键都是递增的
  5. 每个节点的孩子比key的个数多1

这次用 ttt 来表示B树的最小度数(任意节点最少有 ttt 个孩子),看下图就明白了:

这样树的节点构成如下:

 /*** B树中的节点。*/private static class BTreeNode<K, V> {/*** 节点的项,按键非降序存放*/private List<Entry<K, V>> entries;/*** 内节点的子节点*/private List<BTreeNode<K, V>> children;/*** 是否为叶子节点*/private boolean leaf;/*** 键的比较函数对象*/private Comparator<K> kComparator;private BTreeNode() {entries = new ArrayList<>();children = new ArrayList<>();leaf = false;}...

其中每个节点包括键值对的Entry和指向子节点的children指针列表,他们的长度差一,如下图:

而每个节点里的一个Entry就是存放key和value的键值对:

 private static class Entry<K, V> {private K key;private V value;public Entry(K k, V v) {this.key = k;this.value = v;}// getter/setter@Overridepublic String toString() {return key + ":" + value;}}

查询

和排序二叉树的搜索很类似,只是换成多叉和多项。
输入key,记住每个节点的key都是有序的

  1. 从根节点开始找,如果根节点里有,则返回;否则找到对应的下标去子节点递归搜索;
  2. 如果到了叶子节点还没找到,那就找不到。

下面是部分代码,帮助理解,详细请在后面看完整代码:

 public V search(K key) {return search(root, key);}private V search(BTreeNode<K, V> node, K key) {// 在一个节点内搜索SearchResult<V> result = node.searchKey(key);if (result.isExist()) {return result.getValue();} else {if (node.isLeaf()) {return null;} else {// 进入递归search(node.childAt(result.getIndex()), key);}}return null;}

插入

插入稍微复杂一点,涉及到节点满了之后的拆分,也是递归进行的。设我们的 t=2t=2t=2, 则一个节点最多3个key,至少1个key,至多4个孩子。
从1-10进行示例:
先插入1,3,2,形成根节点

再插入4,在插入4之前就判断出此时已经超过最大容量3,需要分割

根据具体实现,我们先分割,再插入,分割会先提出了t−1=1t-1=1t−1=1项也就是3作为新的父节点,再把4插进去,这是个递归的过程,因为4比3大,所以是插入到3的右子树里。

继续插入5

然后是6,发现已经满了,于是分割出4,上升与父节点合并

接着7和8,到8的时候依然先分割,于是我们发现根节点已经满了

我们接着插入9和10,插入10时,7,8,9节点先分割,于是8会上升

但是这时候根节点已满,所以继续递归的拆分,提出4

最终结果如下

关于代码的实现,大家看看源码就明白了。

删除

删除比较麻烦,但是不复杂,只是分的情况比较多,可以分为以下3种:

  1. 要删除的key在叶子节点
  2. 要删除的key在当前节点,但不是叶子
  3. 要删除的key不在当前节点,可能在子节点中

所以按这3种情况,下面分别讨论。

删除1

要删除的key在叶子节点,直接删除就好:

删除8后

删除2

要删除的key在当前节点,但不是叶子,这可以继续分为2种情况

删除2.1

当前节点左或右子节点key数>=t,如下:

我们删除45,从根节点出发,发现45就在根节点,并且它的左右孩子的key数都大于 t=2t=2t=2,于是我们用左孩子的最后一个key 43替换45,然后递归删除左孩子的43.

如果左孩子的key数<t,那就用右孩子的第一个去替换,然后递归删除右孩子的第一个key。

删除2.2

当前节点的左右孩子都只有t-1个key,小于t 。比如下面,我们要删除5,发现5的左右孩子都只有一个key

这时候就可以合并了,合并规则是左孩子的key+要删除的key+右孩子的key,然后合并到左孩子,合并的中间状态如下:(为啥可以合并?因为合并后的key数刚好为2t−12t-12t−1)

然后递归删除左孩子的5,结果如下:

删除3

要删除的key不在当前节点,可能在子节点中. 这依然分2种情况。

删除3.1

当前节点的一个相邻兄弟的key数>=t,如下,右孩子的key数>=2

我们要删除1,于是进行一次左旋(假如是左孩子的key数>=t,则进行右旋),然后递归删除左节点的1(旋转后还没有删除1,下面的图是最终结果)

删除3.2

当前节点的所有相邻兄弟的key数都=t-1,如下,当前在3

于是我们进行合并,类似前面2.2的合并:

再递归的删除3(注意,本例很简单,但很可能2和4节点还有子节点,所以要递归进行,不只是为了删除,也可能是调整结构)

删除如何调整结构

删除还有可能调整结构,即便什么都没删除,下面是一个例子:这是个打印出来的B树

如果我们删除9,9并不存在,但会得到以下结果:

为什么?仔细一分析就知道第一步时就满足删除3.2的情况了,所以进行了合并,这个例子在我源码的测试代码里。
另外,这只是根据实现的不同而不同,不一定都会改变原树。

复杂度分析

简单的分析一下复杂度把。需要先明白一个概念:
B树为访问外存而生,而什么时候访问呢?就在每次访问节点的时候会查一次磁盘IO,节点内的查询是在内存种进行的,而在访问子节点时就需要访问外存,而最多访问h(树的高度)次节点,因为每一层只会访问一个节点。所以,时间都花在访问节点上。

  1. n个keyn个keyn个key,高度为hhh的树,t&gt;=2t&gt;=2t>=2,满足 h&lt;=logt(n+1)/2h&lt;=log_t(n+1)/2h<=logt​(n+1)/2
  2. 查询,插入,删除复杂度:O(th)=O(tlogtn)O(th)=O(tlog_tn)O(th)=O(tlogt​n)

解释:O(h)=O(logtn)O(h)=O(log_tn)O(h)=O(logt​n)次访问外存操作,因为每个节点的key&lt;2tkey &lt; 2tkey<2t 个,所以每个节点的时间为:O(t)O(t)O(t), 每一层只访问一个节点,所以总共最多访问hhh次节点 ,所以复杂度为 O(tlogtn)O(tlog_tn)O(tlogt​n)

完成代码

https://code.csdn.net/snippets/2608045

总结

关于B的含义

这个不是重点,但这个B树直接就出现在论文中了,作者也没做说明,所以只能猜测:

  1. Rudolf Bayer 和 Ed McCreight 于1972年,在Boeing Research Labs 工作时发明了B 树
  2. Balanced(平衡的), broad(宽的) 或 bushy(浓密的)
  3. Bayer:他的名字

参考

  1. https://en.wikipedia.org/wiki/B-tree#CITEREFBayerMcCreight1972
  2. http://blog.jobbole.com/111757/
  3. 绘图网址–注意就是本文只是使用这个网站的B树图例,而实现B树的算法与它不同,所以不要困惑

B树详细图解与Java完整实现相关推荐

  1. 数据结构(十六)——左高树(含合并过程详细图解)

    文章目录 前言 左高树 什么是左高树? 为什么要使用左高树? 最大左高树实现及复杂度分析 节点类和最大左高树类 合并方法 插入(push).删除(pop) 初始化 左高树合并图解 前言 看到左高树的时 ...

  2. 转载:二叉树的前中后和层序遍历详细图解(递归和非递归写法)

    二叉树的前中后和层序遍历详细图解(递归和非递归写法) Monster_ii 2018-08-27 17:01:53 50530 收藏 403 分类专栏: 数据结构拾遗 文章标签: 二叉树 前序 中序 ...

  3. 高端服务器CPU详细图解

    高端服务器CPU详细图解--HP.IBM.SUN篇<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:o ...

  4. 超大超详细图解,让你掌握Spark memeoryStore内存管理的精髓

    摘要:memoryStore主要是将没有序列化的java对象数组或者序列化的byteBuffer放到内存中. 本文分享自华为云社区<spark到底是怎么确认内存够不够用的?超大超详细图解!让你掌 ...

  5. 红黑树模拟软件_【程序员面试必备】红黑树详细图解

    红黑树(Red Black Tree)是一种自平衡的二叉搜索树(Self-balancing Binary Search Tree).以前也叫做平衡二叉 B 树(Symmetric Binary B- ...

  6. 超详细图解!【MySQL进阶篇】存储过程,视图,索引,函数,触发器

    超详细图解![MySQL进阶篇]存储过程,视图,索引,函数,触发器 1.1 下载Linux 安装包 1.2 安装MySQL 1.3 启动 MySQL 服务 1.4 登录MySQL 2\. 索引 2.1 ...

  7. IntelliJ IDEA 最常用配置详细图解,新手入门必看

    转载自   IntelliJ IDEA 最常用配置详细图解,新手入门必看 刚刚使用IntelliJ IDEA 编辑器的时候,会有很多设置,会方便以后的开发,磨刀不误砍柴工. 比如:设置文件字体大小,代 ...

  8. NHibernate使用之详细图解

    本文档适合初级开发者或者是第一次接触NHibernate框架的朋友,其中NHibernate不是最新的版本,但是一个比较经典的版本 NHibernate 2.1.2,其中用红线标注的部分一定要仔细看, ...

  9. 【源码+图片素材+详细教程】Java游戏开发_Java开发经典游戏飞翔的小鸟_飞扬的小鸟_Java游戏项目Flappy Bird像素鸟游戏_Java课程设计项目

    课程目标: 1.通过本课程的学习巩固Java的相关基础知识,例如循环判断,数组和集合的使用,对象的继承,接口的实现,窗口的创建,事件监听,图形绘制. 2.完成小鸟的移动,管道自动生成.碰撞死亡,计分系 ...

最新文章

  1. 央视曝光:股市暴跌暴涨,投资的安全感在哪里?
  2. python3 遍历列表list 四种方法
  3. 【译】你不知道的 Chrome 调试工具技巧 第二十天:Workspace的黑魔法
  4. 包含头文件的问题之1.7编程基础之字符串 24:单词的长度
  5. 第14章 使用Kotlin 进行 Android 开发
  6. java中报错java.sql.Timestamp cannot be cast to java.sql.Date
  7. 机器学习大作业_机器学习编程作业6-支持向量机(Python版)
  8. 机器学习中的numpy的array_Python中机器学习的Numpy指南
  9. vue.js下载安装教程
  10. Linux期末考试试题长沙理工,Linux 期末考试试题4
  11. Latch up 闩锁效应
  12. 工程目录结构模板设计
  13. 提高你修养的100句话
  14. php faker,PHP Faker 教程
  15. 关于电平转换电路1.8V转3.3V
  16. Spring Boot (八): Mybatis 增强工具 MyBatis-Plus
  17. php身份证识别ORC
  18. douyin网页版登录加密分析(一)
  19. Unity实现按Esc键控制面板出现消失,同时游戏暂停
  20. 字节跳动今日头条笔试题目经历2018

热门文章

  1. pureftpd安装文档
  2. 管家婆辉煌ⅡTOP本单据或后续单据已经生成了凭证,不能修改删除
  3. mybatis+servlet
  4. 如何学习python数据分析?
  5. 如何解决卸载McAfee时出现“处于托管模式时无法删除”问题
  6. 图解linux内核设计艺术,Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理...
  7. rapidio学习使用过程摘抄
  8. T568A标准和T568B标准
  9. 仓促的为ic配置了一下编辑器cke和ckf
  10. allegro多版本安装_Revit2020怎么安装?revit2020版本安装教程,图文教程