B树、B+树

when ? why ? how ? what ?

平衡二叉树其查找的时间复杂度是 O(log2N)与树的深度相关,那么降低树的深度自然会提高查找效率。

如果我们要操作的数据集非常大,大到内存已经没法处理了怎么办呢?如数据库中的上千万条记录的数据表、硬盘中的上万个文件等。在这种情况下,对数据的处理需要不断从硬盘等存储设备中调入或调出内存页面。

一旦涉及到这样的外部存储设备,关于时间复杂度的计算就会发生变化,访问该集合元素的时间已经不仅仅是寻找改元素所比较次数的函数,我们必须考虑硬盘等外部存储设备的访问时间以及将会对该设备做出多少次单独访问

我们从外存储器中读取信息的步骤,简单来分,大致有两步:

  1. 找到存储这个数据所对应的磁盘页面,这个过程是机械化的过程,需要依靠磁臂的转动,找到对应磁道,所以耗时长。
  2. 读取数据进内存,并实施运算,这是电子化的过程,相当快。

为什么需要 B 树?

在一个拥有几十万个文件的磁盘中查找一个文本文件,你设计的算法需要读取磁盘上万次还是读取几十次,这是有本质差异。此时,为了降低对外存设备的访问次数,我们就需要新的数据结构(B 树)来处理这样的问题

之前谈的树,都是一个结点可以有多个孩子,但它自身只存储一个元素。二叉树限制更多,结点最多只能有两个孩子。

问题来了? 在元素非常多的时候,要么树的度非常大(结点拥有子树的个数的最大值),要么树的高度非常大,甚至两者都必须足够大才行。这就使得内存存取外存次数非常多,这显然成了时间效率上的瓶颈,这迫使我们要打破每一个结点只存储一个元素的限制,为此引入了多路查找树


多路查找树

什么是多路查找树?

多路查找树(muitl-way search tree),其每一个结点的孩子数可以多于两个,且每个结点出可以存储多个元素。由于它是查找树,所有元素之间存在某种特定的排序关系。

B 树

B 树是一种平衡的多路查找树,2-3树和2-3-4树都是 B 树的特例。结点最大孩子的数目称为 B 树的阶(order),因此,2-3 树是 3 阶 B 树,2-3-4 树是 4 阶 B 树 。

一个 m 阶的 B 树具有如下属性:

  1. 如果根结点不是叶结点,则其至少有两棵子树
  2. 每一个非根的分支结点都有 k-1 个元素和 k 个孩子,其中 ⌈m/2⌉ <= k <=m
  3. 所有的叶子节点都位于同一层
  4. 所有分支结点包含下列信息数据(n,A0,K1,A1,K2,A2....,Kn,An),其中:Ki(i=1,2,3,...n)为关键字,且 Ki < Ki+1(i=1,2,....,n-1);Ai(i=0,2,....n)为指向子树根结点的指针,且指针 Ai-1 所指子树中所有结点的关键字均小于 Ki(i=1,2,....,n),An所指子树中所有结点的关键字均大于 Kn,n(⌈m/2⌉-1<= n <=m-1)为关键字的个数(或n+1为子树的个数)。

2-3-4树

下图灰色方块表示当前结点元素的个数。

rYZmHRa.png

如果内存和外存交换数据次数频繁,会造成了时间效率上的瓶颈,那么 B 树结构怎么就可以做到减少次数呢?

外存比如硬盘,是将所有的信息分割成相等大小的页面,每次硬盘读写的都是一个或多个完整的页面,对于一个硬盘来说,一页的长度可能是 211 或 214 个字节。

在一个典型的 B 树应用中,要处理的硬盘数据量很大,因此无法一次全部装入内存。因此我们会对 B 树 进行调整,使得 B 树的阶数 (或结点的元素)与硬盘存储的页面大小匹配。比如一棵 B 树的阶位 1001(即一个结点包含 1000 个关键字),高度为 2,它可以存储超过 10 亿个关键字,我们只要让根结点持久地保留在内存中,那么在这棵树上,寻找某一个关键字至多需要两次硬盘的读取即可。

通过这种方式,在有限内存的情况下,每一次磁盘的访问我们都可以获取最大数量的数据。由于 B 树每结点可以具有比二叉树多得多的元素,所以与二叉树的操作不同,它们减少必须访问结点和数据块的数量,从而提高了性能。可以说,B树的数据结构就是为内外存的数据交互准备的。

对于有n个关键字的m阶B-树,从根结点到关键字所在结点的路径上路过的结点数不超过:

TS5Fk6E.jpg

B+树

为什么会产生 B+ 树呢?

肯定是 B 树有些缺陷,有些需求然后就产生的 B+ 树。

Egvjm6O.png

如上图的 2-3 树,要是你想查找 3~11 范围的元素,中序遍历 页面 5 -> 页面 2 ->页面 6 -> 页面 1 ->页面 7,查找很麻烦,看看 B+树

AurIR1g.png

当找到查找下限 3 后,通过链表指针可以查找到 11,这样会快很多。

在 B 树中,每一个元素在该树中只出现一次,有可能在叶子结点上,也有可能在分支结点上。而在 B+ 树中,出现在分支结点中的元素会被当作它们在该分支结点位置的中序后继者(叶子结点)中再次列出。另外,每一叶子结点都会保存一个指向后一叶子结点的指针。

卫星数据:指的是索引元素所指向的数据记录,比如数据库中的某一行。

B+ 树的特征:

  1. 有 k 个子树的中间结点包含有 k 个元素(B 树中是 k-1 个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。(在B 树中,无论中间结点还是叶子结点都带有卫星数据)
  2. 所有的叶子结点包含了全部元素的信息,及指向含这些元素的记录的指针,且叶子结点本身依关键字的大小自小而大顺序连接。
  3. 所有的中间结点元素都同时存在余子节点,在子节点元素中是最大(或最小)元素。

B+ 树的优势

  1. B+ 树的中间结点没有卫星数据。所有以同样大小的磁盘可以容纳更多结点元素,这就意味着,数据量相同的情况下,B+ 树的结构比 B- 树更加“矮胖”,因此查询是 IO次数也更少。
  2. B- 树查找性能不稳定(最好情况只查根结点,最坏情况查到叶子结点),而B+ 树每次查找都是最稳定的。
  3. 所有叶子节点形成有序链表,便于范围查询。如查找学小18~22岁的学生人数,我们可以通过根结点触发找到第一个18岁学生,然后再在叶子结点按顺序查找到符合范围的所有记录

总结

参考

大话数据结构

https://www.sohu.com/a/156886901_479559

B树是为实现高效的磁盘存取而设计的多叉平衡搜索树。这个概念在文件系统,数据库系统中非常重要。数据库中的索引就用了B树或B+树。

有什么问题欢迎指出,十分感谢!

转载于:https://www.cnblogs.com/rookieJW/p/9355791.html

### BB+的概念 B是一种自平衡的多路查找,其设计目的是为了减少磁盘I/O操作次数。它通过确保所有叶子节点处于同一层来维持平衡性[^1]。B+是B的一种变体,保留了B的基本特性,同时在结构上进行了改进以优化范围查询和顺序访问性能[^3]。 ### BB+的区别 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、付费专栏及课程。

余额充值