B树、B+树

前言

        B树和B+树都是平衡的多路搜索树,它们在数据库和文件系统中广泛使用,用于存储和检索数据。B是指balance,也就是平衡的意思。那这俩与平衡二叉树有啥区别?首先要知道AVL树与B树、B+树他们都是自平衡搜索树。

  • ALV的子树间高度不会超过1,通过判断每个节点的平衡因子(左子树的高度减去右子树的高度)来保持平衡,如果任何节点的平衡因子的绝对值超过1,则需要通过旋转(左旋、右旋、双旋)操作来重新平衡树。
  • 而B树、B+树,是多路平衡查找树,每个节点可以有多个子节点,节点可以包含多个键和指向子节点的指针。键将子节点分割成不同的范围,B树通过确保所有叶子节点都在同一层也就是分裂节点和合并节点来保持平衡。

        其实很容易发现,如果在数据库逻辑里面,大量的节点如果用AVL树存储,树的整体高度就会超级高,而B树、B+树是矮胖结构,键越多越胖。所以B树、B+树非常适合用于数据库索引和文件系统的目录结构,因为它们可以减少磁盘I/O操作的次数,迎合磁盘IO的需求,因为磁盘不比内存,访问速度是很慢的,这时候如果使用AVL树,如此高度会大大降低整体性能,此外,AVL树的旋转操作会使逻辑上很近的节点离得很远,这时候就违背了局部性原理(如果一个存储器的某个位置被访问,那么将它附近的位置也会被访问)。到这里我们应该了解了,B树、B+树有个key-value的概念,key用于查找,value用于存储数据。

B树

  • 节点:B树的节点可以包含多个键和指向子节点的指针。每个节点的键值数量可以变化。
  • 平衡:B树通过分裂节点来保持平衡,确保所有叶子节点都在同一层或者相邻叶子节点的层级差不超过1。
  • 搜索:B树支持快速的搜索操作,因为每个节点都保存了键值,可以直接进行比较。
  • 应用:B树常用于文件系统和数据库索引,因为它可以有效地减少磁盘I/O操作。
  • 插入:如果节点未满,直接插入;如果节点已满,进行分裂
  • 删除:找到要删除的键值,如果节点不满,可能需要合并节点。
  • 搜索:从根节点开始,比较键值,沿着指针向下直到找到键值或叶子节点。

        前面我们有讨论到,B树设计是为了满足磁盘使用的要求,就是提高效率,而快速索引恰好需要减少磁盘IO的次数,AVL树的搜索层次在数据量大的时候就肯定会比B树的搜索层次高很多,而每个节点都需要进行磁盘IO,这样看来B+树在这种情况下是力压AVL树的。

B树的分裂和合并是B树维护其平衡性的关键操作。当B树进行插入操作导致某个节点的键值数量超过最大限制时,需要进行分裂;而当删除操作导致节点的键值数量低于最小限制时,可能需要进行合并。

B树的分裂

        分裂操作发生在插入新键值时,如果插入后节点的键值数量超过了节点的最大容量(通常记为M),则需要分裂该节点。

  1. 选择中间键值:选择中间的键值(假设有K个键值,则选择第(M+1)/2个键值,其中M是节点的最大键值容量)。
  2. 分割节点:将中间键值以及其右侧的所有键值移动到新的节点中。
  3. 更新父节点:将中间键值上移至父节点,作为两个子节点的分隔键值。
  4. 处理子节点:新的节点成为原节点的兄弟节点,并且它们共享相同的父节点。

分裂步骤示例

  1. 假设一个节点有键值A, B, C, D, E,最大容量为3,需要插入键值F。
  2. 插入F后,节点变为A, B, C, D, E, F,超过了最大容量。
  3. 选择中间键值C,将C, D, E, F移动到新的节点。
  4. 将C上移至父节点,原节点变为A, B,新节点为D, E, F。

B树的合并

        合并操作发生在删除键值时,如果删除后某个节点的键值数量低于最小限制(通常记为M/2),则需要与兄弟节点合并。

  1. 检查兄弟节点:查看兄弟节点的键值数量。
  2. 借用键值:如果兄弟节点的键值数量大于最小限制,可以从兄弟节点借用一个键值到当前节点。
  3. 合并节点:如果兄弟节点的键值数量等于最小限制,可以将当前节点与一个兄弟节点合并
  4. 更新父节点:如果合并了节点,需要删除父节点中指向当前节点的键值,并调整指针。

合并步骤示例

  1. 假设一个节点有键值A, B,最小容量为2,需要删除键值A。
  2. 删除A后,节点变为B,低于最小容量。
  3. 检查兄弟节点,假设兄弟节点有键值C, D, E。
  4. 如果兄弟节点的键值数量大于最小限制,可以将C借用到当前节点,当前节点变为B, C。
  5. 如果兄弟节点的键值数量等于最小限制,可以将当前节点与兄弟节点合并,合并后的节点为B, C, D。

B+树

  • 节点:B+树的非叶子节点不存储数据,只存储键值和指向子节点的指针。所有的数据都存储在叶子节点中。
  • 平衡:B+树同样保持所有叶子节点在同一层。
  • 链表:B+树的叶子节点之间通过指针连接,形成一个链表,这使得顺序访问非常高效。
  • 搜索:B+树在搜索操作上与B树相似,但由于所有数据都在叶子节点,所以对于范围查询更加高效。
  • 应用:B+树特别适合用于数据库索引和文件系统的存储,因为它可以减少磁盘I/O操作,提高数据读取效率。
  • 插入:首先在叶子节点中插入,如果叶子节点满了,则分裂节点,并且更新父节点。
  • 删除:首先在叶子节点中删除,如果叶子节点不满,可能需要从兄弟节点借键或者合并节点。
  • 搜索:从根节点开始,沿着键值向下直到叶子节点,然后可以在链表中顺序访问。

B+树的合并与分裂操作与B树类似,但有一些关键区别。B+树的特点是所有数据都存储在叶子节点,并且叶子节点之间通过指针连接,形成一个链表。这种结构使得B+树在处理大量数据时更加高效,尤其是在进行范围查询和顺序访问时。

B+树的分裂

分裂操作发生在插入新键值时,如果插入后某个节点的键值数量超过了节点的最大容量(通常记为M),则需要分裂该节点。

  1. 选择中间键值:选择中间的键值(假设有K个键值,则选择第 ⌈(K+1)/2⌉⌈(K+1)/2⌉ 个键值)。
  2. 分割节点:将中间键值以及其右侧的所有键值移动到新的节点中。
  3. 更新父节点:将中间键值上移至父节点,作为两个子节点的分隔键值。
  4. 处理叶子节点:在B+树中,叶子节点之间通过指针连接。分裂操作后,需要更新这些指针以保持链表的连续性。

分裂步骤示例

  1. 假设一个节点有键值A, B, C, D, E,最大容量为3,需要插入键值F。
  2. 插入F后,节点变为A, B, C, D, E, F,超过了最大容量。
  3. 选择中间键值C,将C, D, E, F移动到新的节点。
  4. 将C上移至父节点,原节点变为A, B,新节点为D, E, F。
  5. 更新叶子节点的指针,确保链表的连续性。

B+树的合并

合并操作发生在删除键值时,如果删除后某个节点的键值数量低于最小限制(通常记为M/2),则需要与兄弟节点合并。

  1. 检查兄弟节点:查看兄弟节点的键值数量。
  2. 借用键值:如果兄弟节点的键值数量大于最小限制,可以从兄弟节点借用一个键值到当前节点。
  3. 合并节点:如果兄弟节点的键值数量等于最小限制,可以将当前节点与一个兄弟节点合并。
  4. 更新父节点:如果合并了节点,需要删除父节点中指向当前节点的键值,并调整指针。
  5. 处理叶子节点:在B+树中,叶子节点之间通过指针连接。合并操作后,需要更新这些指针以保持链表的连续性。

合并步骤示例

  1. 假设一个节点有键值A, B,最小容量为2,需要删除键值A。
  2. 删除A后,节点变为B,低于最小容量。
  3. 检查兄弟节点,假设兄弟节点有键值C, D, E。
  4. 如果兄弟节点的键值数量大于最小限制,可以将C借用到当前节点,当前节点变为B, C。
  5. 如果兄弟节点的键值数量等于最小限制,可以将当前节点与兄弟节点合并,合并后的节点为B, C, D。
  6. 更新叶子节点的指针,确保链表的连续性。
### B与B+的概念 B是一种自平衡的多路查找,其设计目的是为了减少磁盘I/O操作次数。它通过确保所有叶子节点处于同一层来维持平衡性[^1]。B+是B的一种变体,保留了B的基本特性,同时在结构上进行了改进以优化范围查询和顺序访问性能[^3]。 ### B与B+的区别 1. **节点存储结构** 在B中,每个节点既可以存储数据(键值对),也可以作为索引使用。而在B+中,内部节点仅存储索引信息,所有数据记录都存储在叶子节点中[^2]。 2. **叶子节点链表** B+的所有叶子节点通过指针链接形成一个有序链表,这使得范围查询和顺序扫描更加高效。而B不具备这样的特性,导致其在范围查询时效率较低[^3]。 3. **数据分布** B的数据分布在所有节点中,包括非叶子节点。而B+的数据只集中在叶子节点上,这种设计减少了重复存储的可能性,并提高了空间利用率[^1]。 4. **查询效率** 对于单点查询,B和B+的表现差异不大。然而,在范围查询场景下,B+由于其叶子节点链表的设计,能够显著提升性能[^2]。 5. **插入与删除操作** B+在插入和删除时,由于数据集中存储在叶子节点上,调整操作相对简单,可以避免频繁地调整非叶子节点中的数据。 ### 实现细节 以下是一个简单的B+实现示例,展示了如何构建和查询: ```python class BPlusTreeNode: def __init__(self, is_leaf=False): self.keys = [] self.children = [] self.is_leaf = is_leaf self.next_leaf = None # 指向下一个叶子节点 class BPlusTree: def __init__(self, t): self.root = BPlusTreeNode(True) self.t = t def insert(self, key): root = self.root if len(root.keys) == (2 * self.t) - 1: new_node = BPlusTreeNode() self.root = new_node new_node.children.append(root) self.split_child(new_node, 0) self.insert_non_full(new_node, key) else: self.insert_non_full(root, key) def split_child(self, node, index): t = self.t child = node.children[index] new_node = BPlusTreeNode(child.is_leaf) node.children.insert(index + 1, new_node) node.keys.insert(index, child.keys[t - 1]) new_node.keys = child.keys[t:(2 * t) - 1] child.keys = child.keys[0:t - 1] if not child.is_leaf: new_node.children = child.children[t:(2 * t)] child.children = child.children[0:t] def insert_non_full(self, node, key): i = len(node.keys) - 1 if node.is_leaf: node.keys.append(None) while i >= 0 and key < node.keys[i]: node.keys[i + 1] = node.keys[i] i -= 1 node.keys[i + 1] = key else: while i >= 0 and key < node.keys[i]: i -= 1 i += 1 if len(node.children[i].keys) == (2 * self.t) - 1: self.split_child(node, i) if key > node.keys[i]: i += 1 self.insert_non_full(node.children[i], key) ``` ### 应用场景 - **B的应用场景** B适用于需要频繁进行随机访问的场景,例如文件系统中的元数据管理、数据库的索引结构等。由于其支持快速的单点查询,因此在某些特定场景下仍然具有优势[^1]。 - **B+的应用场景** B+广泛应用于数据库系统(如MySQL的InnoDB引擎)和文件系统中。它的优点在于能够高效处理范围查询和顺序访问,因此非常适合大规模数据集的索引构建[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值