B树和B+树

本文探讨了B-树和B+树这两种自平衡搜索树,重点讲解了B树的多路平衡设计、磁盘优化特性以及查询效率。B树适合外部存储,而B+树内节点不存储数据,查询性能稳定为O(logn),特别利于范围查询和利用局部性原理。

1、B-树

1.1 B-树概述

B-树,这里的 B 表示 balance( 平衡),B-树是一种多路自平衡的搜索树。B-树允许每个节点有更多的子节点。B-树是专门为外部存储器设计的,如磁盘,它对于读取和写入大块数据有良好的性能,所以一般被用在文件系统及数据库中。
在这里插入图片描述
一个m阶的B树具有如下几个特征:

  • 根结点至少有两个子女。

  • 每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m

  • 每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m

  • 所有的叶子结点都位于同一层。

  • 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

B-树有如下特点:

  • 所有键值分布在整颗树中(索引值和具体data都在每个节点里);
  • 任何一个关键字出现且只出现在一个结点中;
  • 搜索有可能在非叶子结点结束(最好情况O(1)就能找到数据);
  • 在关键字全集内做一次查找,性能逼近二分查找;

1.2 B树深入

传统用来搜索的平衡二叉树有很多,如 AVL 树,红黑树等。这些树在一般情况下查询性能非常好,但当数据非常大的时候它们就无能为力了。原因当数据量非常大时,内存不够用,大部分数据只能存放在磁盘上,只有需要的数据才加载到内存中。一般而言内存访问的时间约为 50 ns,而磁盘在 10 ms 左右。速度相差了近 5 个数量级,磁盘读取时间远远超过了数据在内存中比较的时间。这说明程序大部分时间会阻塞在磁盘 IO 上。那么我们如何提高程序性能?减少磁盘 IO 次数,像 AVL 树,红黑树这类平衡二叉树从设计上无法“迎合”磁盘。

我们从“迎合”磁盘的角度来看看B-树的设计

索引的效率依赖与磁盘 IO 的次数,快速索引需要有效的减少磁盘 IO 次数,如何快速索引呢?索引的原理其实是不断的缩小查找范围,就如我们平时用字典查单词一样,先找首字母缩小范围,再第二个字母等等。平衡二叉树是每次将范围分割为两个区间。为了更快,B-树每次将范围分割为多个区间,区间越多,定位数据越快越精确。那么如果节点为区间范围,每个节点就较大了。所以新建节点时,直接申请页大小的空间(磁盘存储单位是按 block 分的,一般为 512 Byte。磁盘 IO 一次读取若干个 block,我们称为一页,具体大小和操作系统有关,一般为 4 k,8 k或 16 k),计算机内存分配是按页对齐的,这样就实现了一个节点只需要一次 IO。
在这里插入图片描述
上图是一棵简化的B-树,多叉的好处非常明显,有效的降低了B-树的高度,为底数很大的 log n,底数大小与节点的子节点数目有关,一般一棵B-树的高度在 3 层左右。层数低,每个节点区确定的范围更精确,范围缩小的速度越快(比二叉树深层次的搜索肯定快很多)。上面说了一个节点需要进行一次 IO,那么总 IO 的次数就缩减为了 log n 次。B-树的每个节点是 n 个有序的序列(a1,a2,a3…an),并将该节点的子节点分割成 n+1 个区间来进行索引(X1< a1, a2 < X2 < a3, … , an+1 < Xn < anXn+1 > an)。

点评:B树的每个节点,都是存多个值的,不像二叉树那样,一个节点就一个值,B树把每个节点都给了一点的范围区间,区间更多的情况下,搜索也就更快了,比如:有1-100个数,二叉树一次只能分两个范围,0-50和51-100,而B树,分成4个范围 1-25, 25-50,51-75,76-100一次就能筛选走四分之三的数据。所以作为多叉树的B树是更快的

1.3 B-树的查找

我们来看看B-树的查找,假设每个节点有 n 个 key值,被分割为 n+1 个区间,注意,每个 key 值紧跟着 data 域,这说明B-树的 key 和 data 是聚合在一起的。一般而言,根节点都在内存中,B-树以每个节点为一次磁盘 IO,比如上图中,若搜索 key 为 25 节点的 data,首先在根节点进行二分查找(因为 keys 有序,二分最快),判断 key 25 小于 key 50,所以定位到最左侧的节点,此时进行一次磁盘 IO,将该节点从磁盘读入内存,接着继续进行上述过程,直到找到该 key 为止。

2、B+ 树

2.1 B+树概述

B+树是B-树的变体,也是一种多路搜索树, 它与 B- 树的不同之处在于:

  • 所有关键字存储在叶子节点出现,内部节点(非叶子节点并不存储真正的 data)
  • 为所有叶子结点增加了一个链指针
    在这里插入图片描述
    在这里插入图片描述
    因为内节点并不存储 data,所以一般B+树的叶节点和内节点大小不同,而B-树的每个节点大小一般是相同的,为一页。

为了增加 区间访问性,一般会对B+树做一些优化。
如下图带顺序访问的B+树。
在这里插入图片描述
一个m阶的B+树具有如下几个特征:

  • 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。

  • 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

  • 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

2.2 B-树和B+树的区别

1.B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)。

如下所示B-树/B+树查询节点 key 为 50 的 data。

B-树:
在这里插入图片描述
从上图可以看出,key 为 50 的节点就在第一层,B-树只需要一次磁盘 IO 即可完成查找。所以说B-树的查询最好时间复杂度是 O(1)。

B+树:
在这里插入图片描述
由于B+树所有的 data 域都在根节点,所以查询 key 为 50的节点必须从根节点索引到叶节点,时间复杂度固定为 O(log n)。

点评:B树的由于每个节点都有key和data,所以查询的时候可能不需要O(logn)的复杂度,甚至最好的情况是O(1)就可以找到数据,而B+树由于只有叶子节点保存了data,所以必须经历O(logn)复杂度才能找到数据

2. B+树叶节点两两相连可大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找。
在这里插入图片描述
根据空间局部性原理:如果一个存储器的某个位置被访问,那么将它附近的位置也会被访问。

B+树可以很好的利用局部性原理,若我们访问节点 key为 50,则 key 为 55、60、62 的节点将来也可能被访问,我们可以利用磁盘预读原理提前将这些数据读入内存,减少了磁盘 IO 的次数。
当然B+树也能够很好的完成范围查询。比如查询 key 值在 50-70 之间的节点。

点评:由于B+树的叶子节点的数据都是使用链表连接起来的,而且他们在磁盘里是顺序存储的,所以当读到某个值的时候,磁盘预读原理就会提前把这些数据都读进内存,使得范围查询和排序都很快

3.B+树更适合外部存储。由于内节点无 data 域,每个节点能索引的范围更大更精确

这个很好理解,由于B-树节点内部每个 key 都带着 data 域,而B+树节点只存储 key 的副本,真实的 key 和 data 域都在叶子节点存储。前面说过磁盘是分 block 的,一次磁盘 IO 会读取若干个 block,具体和操作系统有关,那么由于磁盘 IO 数据大小是固定的,在一次 IO 中,单个元素越小,量就越大。这就意味着B+树单次磁盘 IO 的信息量大于B-树,从这点来看B+树相对B-树磁盘 IO 次数少。

点评:由于B树的节点都存了key和data,而B+树只有叶子节点存data,非叶子节点都只是索引值,没有实际的数据,这就时B+树在一次IO里面,能读出的索引值更多。从而减少查询时候需要的IO次数!

参考:
一文彻底搞懂MySQL基础:B树和B+树的区别
B+树遍历与查找

### B与B+的区别 BB+都是多路平衡查找,适用于外部存储大规模据索引,但它们在结构功能上存在显著差异。 #### 1. **关键字与节点关系** B中,每个节点既可以存储关键字,也可以存储据。对于一个m阶的B来说,每个节点最多包含m-1个关键字,同时子节点量也最多为m。而B+的每个节点可以包含m个关键字,并且所有据都存储在叶子节点中。非叶子节点仅存储索引信息,这使得B+更适用于大据量的索引结构。 #### 2. **据存储位置** B据可以存储在任意节点中,包括根节点、中间节点叶子节点。而B+据仅存储在叶子节点中,中间节点仅用于索引。这种设计使得B+能够更高效地进行批量据访问。 #### 3. **分支节点的构造** B的分支节点存储关键字子节点指针,同时可能存储据。而B+的分支节点仅存储关键字子节点指针,不存储实际据。这意味着在相同的页大小(例如16KB)下,B+可以存储更多的索引信息,从而减少的高度,提高查询效率。 #### 4. **查询性能** B查询时,可能在非叶子节点就找到所需据,因此查询路径较短。但B+查询路径固定,从根节点到叶子节点的高度一致,因此查询性能更加稳定。这种稳定性对于据库索引尤为重要。 #### 5. **区间访问** B+的叶子节点通过链表连接,支持高效的区间查询。例如,据库中的范围查询(如`SELECT * FROM table WHERE id BETWEEN 100 AND 200`)可以利用B+的这一特性快速完成。而B的叶子节点之间没有链接,区间查询效率较低。 #### 6. **磁盘I/O效率** 由于B+的非叶子节点不存储据,每个节点可以容纳更多的关键字,从而减少的高度。这使得B+在磁盘I/O操作上更加高效,尤其适合需要频繁读取外部存储的场景。 ### B与B+的应用场景 #### B的应用场景 B适用于需要频繁更新查询的小规模据集。由于B的非叶子节点存储据,它在某些特定的场景中具有优势,例如: - **内存据库**:当据量较小且需要快速访问时,B可以减少磁盘I/O操作。 - **嵌入式系统**:在资源受限的环境中,B的简单结构可以提供较高的性能[^3]。 #### B+的应用场景 B+由于其结构特性,广泛应用于大规模据存储索引系统,主要包括: - **据库索引**:如MySQL的InnoDB引擎使用B+作为主键索引的底层实现,支持高效的查询范围扫描。 - **文件系统**:B+的叶子节点链表结构支持高效的文件顺序访问,例如在Linux文件系统中。 - **分布式存储系统**:B+的稳定查询性能高效的区间访问能力使其成为分布式据库存储系统的首选索引结构[^2]。 ### 示例代码:B+的插入操作 以下是一个简化的B+插入操作的Python实现,用于展示B+的基本结构插入逻辑: ```python class BPlusTreeNode: def __init__(self, order, is_leaf): self.order = order self.is_leaf = is_leaf self.keys = [] self.children = [] def insert(self, key): if self.is_leaf: self.keys.append(key) self.keys.sort() else: child = self.get_child_for_key(key) child.insert(key) if len(child.keys) >= self.order: self.split_child(child) def split_child(self, child): new_node = BPlusTreeNode(child.order, child.is_leaf) mid = len(child.keys) // 2 new_node.keys = child.keys[mid:] child.keys = child.keys[:mid] if not child.is_leaf: new_node.children = child.children[mid:] child.children = child.children[:mid] self.keys.append(new_node.keys[0]) self.keys.sort() index = self.children.index(child) self.children.insert(index + 1, new_node) def get_child_for_key(self, key): for i, k in enumerate(self.keys): if key < k: return self.children[i] return self.children[-1] class BPlusTree: def __init__(self, order): self.order = order self.root = BPlusTreeNode(order, is_leaf=True) def insert(self, key): self.root.insert(key) if len(self.root.keys) >= self.order: new_root = BPlusTreeNode(self.order, is_leaf=False) new_root.keys = [self.root.keys[0]] new_root.children = [self.root] self.root = new_root # 示例:创建一个B+并插入几个键 tree = BPlusTree(order=4) for key in [10, 20, 5, 6, 15, 30]: tree.insert(key) ``` ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值