首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >BTree实现原理

BTree实现原理

作者头像
数据小冰
发布2022-08-15 15:00:30
发布2022-08-15 15:00:30
1.5K0
举报
文章被收录于专栏:数据小冰数据小冰

小编在看etcd存储(store)模块的时候,发现它在进行key和keyIndex转换的时候,用到了btree包(http://godoc.org/github.com/google/btree)。btree是Google开源的一个Go语言的BTree实现,整个代码不到1000行,实现的非常简练,组织分层也做的很好,并对gc和并发读写做了很多优化,值得一读。小编打算用两篇文章讲解BTree内容,本文上篇主要介绍实现原理,下篇主要介绍btree源码实现。

BTree定义

BTree和B-Tree都是指B树,不要把B-Tree理解成了B-树。关于BTree在《计算机程序设计与艺术》和《算法导论》对其描述是不一样的。一种是通过"阶"的概念来定义的,另一种是通过"度"的概念定义的,两者描述的本质是一样的。

我们先来看通过阶的概念的定义,阶指一个节点的最大子树的个数,定义如下:

❝树中每个节点至多有m颗子树 若根节点不是叶子节点,则至少有两颗子树 除根节点之外的所有非终端节点至少有「m/2」颗子树 所有的非终端节点中包含关键字和指向子树根节点的指针 所有叶子节点在同一个层次上 ❞

下面是算法导论中通过度来定义BTree, 度是一个节点子树的个数,对于一颗最小度为t(t>=2)的BTree,节点中的关键字为key,有如下限制:

❝除了根节点之外,其他节点的t-1<=len(key)<=2t-1 非叶子节点的子树个数t<=len(children)<=2t ❞

两者有点细微的差别,根据《计算机程序设计艺术》的定义,最小的树为3阶BTree,即树的非叶子节点的子树的个数为2或3,根据《算法导论》的定义,最小的树为2度数,对应的前者的阶数为4,即树的非叶子节点个数可以为2、3或者4. etcd中用到的btree包是按这里的度的定义实现的。下图是一个度为3的BTree,除了叶子节点,每个节点的子树个数不是2个就是3个,0004节点的子树有2个,0047|0051节点的子树有3个。

BTree使用场景

BTree常用于实现数据库索引,例如在MongoDB中的索引是用BTree实现的,MySQL中的innodb存储引擎用B+树存储索引信息。通过前面的定义可以看到,BTree是一种平衡多路查找树,与AVL树和红黑树等二叉树比较起来,BTree通过多叉,降低了树的高度,从而减少了查询的次数。为啥数据库的索引采用BTree实现呢?因为数据库的索引信息以树形结构存放在磁盘上,对于高度为h的树,最多需要进行h次查找,对于存放在磁盘上的文件来说,需要读取磁盘h次,而读取磁盘的操作与操作内存相比是很慢的,一次磁盘读取耗时为寻道时间+旋转磁头时间+数据读取传输耗时。普通的机械硬盘,寻道时间大概在10毫秒左后,旋转磁头时间就是磁头移动到对应磁道后,旋转到对应扇区所需要的时间,可以看到整个磁盘IO操作是个很耗时的过程。而BTree降低了树的高度,减少了磁盘读取次数,所以数据库的索引采用BTree或B+树实现。

BTree实现原理

BTree的核心操作包含树的创建,树中节点的删除,元素的查找。下面分别分析每个操作的具体实现。

插入

插入操作是向BTree中插入一条记录,即向里面添加一个key-value键值对。如果要插入的key在BTree中已存在,则用当前的value更新之前的旧value.如果BTree中不存在这个key,则在它的叶子节点中进行插入操作。具体算法流程如下:

  1. 根据要插入的key的值,在BTree查找key存不存,如果已存在,用新的value覆盖旧的value,操作结束
  2. 如果流程1中,没有找到key,则定位到要插入的叶子节点并插入
  3. 判断2中刚插入节点key的个数是否满足BTree的性质,如果不满足,则执行下面的第4步操作
  4. 以插入节点中间的key为中心,分裂成左右两部分,然后将中间的key插入到它的父节点中,这个key的左子树指向分裂后的左半部分,右子树指向分裂后的右半部分。然后继续判断父节点满不满足BTree性质,如果不满足,继续对父节点进行分裂,否则流程执行结束

下面以度(Degree=3)BTree的创建过程为例,介绍插入过程。

向BTree中插入4,插入后只有一个key,因为这是首次插入。

向BTree中插入51,直接将51加入与4同节点中,此时该节点有2个key,满足每个节点不超过2个key的性质.

向BTree中插入38, 度为3的BTree,每个节点最多有2个key, 此时节点有3个key,不满足BTree性质,将中间的key提升到父节点中,调整之后符合BTree定义,插入操作结束。

向BTree中插入43,添加到叶子节点51所在的位置。

向BTree中插入48,添加48到43|51所在的节点后,此时该节点不满足BTree性质,对其进行拆分,将中间的48加入到父节点(38所在的节点),43|48|51节点中的key被分成43和51两部分,48的左右子树分别指向它。

向BTree中插入1

向BTree中插入10,此时1|4|10节点不满足BTree性质,需要进行分裂,将4插入到父节点中,插入之后,父节点4|30|48也不满足BTree性质,继续对其进行分裂。

插入的核心就是当节点的key的数量不满足BTree性质时,向上进行分裂,即将当前节点的中间key插入到父节点中,这样能够确保分裂之后节点的层次深度保持不变。如果父节点也不满足BTree性质,继续对其分裂,这样一路向上,直到根节点,保证整颗树🌲层次一致,即多路平衡性。

删除

从BTree中删除一个元素,根据删除元素的所在的节点是叶子节点还是非叶子节点分为两种情况。下面对这两种情况做一个简单的分析:

  1. 删除元素在非叶子节点:将下面BTree中的元素38删除,如果直接删除,这时候root节点只要一个元素了,但它有3个子节点,不满足BTree性质。那怎么做呢?可以将以38为根节点的子树的最右侧叶子节点的最后一个元素放入到38的位置,然后从叶子节点中删除放入的元素,这时候完美的符合BTree性质,整个数又是平衡的。对应到这里就是将元素4覆盖掉38,然后叶子节点1|4中的4元素删除。

删除元素38后,得到新的BTree树如下

但是这里有一种特殊情况,如果将要删除的元素的子树的最右侧的叶子节点的元素移走之后,可能会导致叶子节点为空,此时叶子节点不满足BTree性质。这种情况只做上面的处理还不够,还要做其他处理。这种情况在2中删除叶子节点的元素导致叶子节点为空也存在,所以放到下面一起分析说明。

  1. 删除元素在叶子节点:如果要删除的元素所在的叶子节点,在删除该元素之后不为空,则可以直接将其删除,如果删除之后,叶子节点变为了空,这时还要做其他处理,上面1中的特殊情况下也可能存在该问题。

处理方法是,如果该叶子节点的兄弟节点有富余的元素,可以借一个过来,如果兄弟节点也没有富余的元素,这时候要对叶子节点进行合并。

下面的BTree删除叶子节点中的元素z, 删除z之后节点C不满足BTree性质。假定该BTree是m阶BTree,根据BTree性质,非根节点元素的个数n, 则n满足ceil(m/2)-1<=n<=m-1. 那说明删除z之后,节点C的元素个数为 ceile(m/2)-2. 其兄弟节点A的元素个数 > ceil(m/2)-1,即节点A最少元素个数为 ceil(m/2),将元素x加入到节点C元素的最左边,然后将节点A最后一个元素y放入到x位置,最后将节点A中的元素y删除,此时又是BTree了。

删除元素z之前

删除元素z之后

上面讨论的是叶子节点的兄弟节点有富余的元素,可以从兄弟节点借。如果兄弟节点的元素也不富余的情况怎么处理呢?嗯,方法是将叶子节点进行合并。下面的m阶BTree中删除叶子节点C中的元素z之后,不满足BTree性质,说明此时节点C在删除z之后剩余的节点数为ceil(m/2)-2, 其兄弟节点A没有富余元素,说明节点A的元素个数为ceil(m/2)-1.处理方法是将元素x下移,与节点A和节点C合并,合并之后的节点我们记为节点AC,则节点AC的元素个数为 ceil(m/2)-1+ceil(m/2)-2+1 = ceil(m/2)+ceil(m/2)-2, 可以证明合并之后的节点数是小等于m-1的,即ceil(m/2)+ceil(m/2)-2<=m-1. 这样合并之后,又是BTree了。如果合并之后节点B不满足BTree性质,因为从节点B移走了元素x, 则处理方式就是这里介绍的两种方法,从节点B的兄弟节点借,如果兄弟节点没有富余元素,则继续合并,最后直到根节点,会将BTree的高度降低。

删除元素z之前

删除元素z之后

下面是一颗度为3的BTree, 现在删除元素90

删除90之后,叶子节点变为空,此时不满足BTree性质,根据前面介绍的处理方法,尝试从它的兄弟节点借一个元素,但是它的兄弟节点也只有一个元素44,只能进行合并处理,将父节点的元素45和44合并。

但此时父节点中的元素为空了,不满足BTree性质,于是对父节点采用从它的兄弟节点借或者合并的方法,而此时它的兄弟节点中也只有一个元素22,所以只能进行合并,将根节点的中的元素41和21合并,BTree的高度减少一层,得到如下的BTree.

查找

BTree是一种多路平衡树,同时也满足有序性,对于每个节点,它左边子树的所有元素都小于该节点中最小的元素,它右边子树的所有元素都大于该节点中最大的元素。每个节点的内部元素也是有序的。所以采用类似二叉树的中序遍历,得到元素一定是有序的。所以BTree中查找元素的过程很简单,从根节点开始,每次可以定位可能所在的1个子节点,这样一路向下查询,如果在内部节点中没有找到,最后达到叶子节点,如果叶子节点也没有,则说明要查询的元素不在BTree中,整个查找过程很简单,这里就不在画图举例说明了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-11-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • BTree定义
  • BTree使用场景
  • BTree实现原理
    • 插入
    • 删除
    • 查找
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档