1. 磁盘I/O问题
众所周知,相较于计算机指令的执行速度,访问磁盘所带来的开销是十分巨大的。对于一段需要访问磁盘的程序,程序运行的时间几乎都花费再了磁盘的I/O上。 也就是说,在这种情况下,如果我们可以将磁盘的访问次数减少一般,那么程序的运行时间也将减半。
当数据量较少,可以完全存储在内存时,数据的访问相对来说是比较快的,类似AVL树、红黑树等数据结构可以将操作数据的时间复杂度控制在O(log2N)。当数据量很大时,无法将其存储在内存当中,只能退而求其次,存储在磁盘中;由于磁盘I/O速度的限制,此时大O模型将不再适用,磁盘的I/O次数决定了程序的时间成本。
针对数据被存储在磁盘中的情况,常规的平衡二叉树已经不能很好的满足我们的需求。因此,我们需要一种新的查找树结构,这种新的树结构可以在相同数据量时拥有更低的高度(树的高度决定了数据操作的最大访问次数)。
2. B-树
为降低树的高度,很自然的一个想法就是让树有更多的分支,B-树就遵循了此想法——既然一个节点有两个分支不够,那就让它拥有尽可能多的分支。B-树的所有节点都携带数据(这也是它和B+树的主要区别),由于它是一颗查找树,因此需要一个“排序”的依据,将此排序的依据成为关键码,每个关键码都关联着具体的数据元素。也就是说,节点的关键码个数也代表着节点存储的数据元素的个数。
B-树又被称为B树,B代表Balanced,即平衡的意思。一棵M阶的B-树有如下的特点:
-
根节点或是一片叶子,或是拥有 2 到 M 个个孩子;
-
除根外,所有非叶子节点都包含k-1个关键码和k个孩子,其中 ceil(M/2) <= k <= M(ceil表示向上取整,如ceil(2.5) = 3);
-
每一个叶子节点都包含k-1个关键码,其中 ceil(M/2) <= k <= M;
-
所有的叶子结点都位于同一层;
-
每个节点中的关键码都遵循从小到大的排列,节点中的k-1个关键码正好可以划分出k个孩子。
单纯的看B-树的定义显得比较的抽象,可以通过具体的插入和删除操作进一步的理解B-树的结构。
2.1 B-树的插入操作
简单的来说,B-数的插入操作可以分为以下几种情况:
-
插入后不破坏B-树规则,则直接插入;
-
插入后该节点的关键码个数超出规定范围(M-1),则进行分裂操作;
-
分裂操作后相当于父节点插入了一个新元素,递归的向上执行插入逻辑。
以M = 5为例(即一颗5阶的B-树),图例中蓝色代表新插入的元素,绿色代表插入前就已经存在的元素:
-
向空树中插入2(插入根节点):
-
继续依次插入1、10、3(找到合适的位置插入即可),可以看到根节点中的关键码遵循从小到大的排列:
-
继续插入9后,此时根节点的关键码个数增加到5个(大于M-1),需要执行分裂操作(找到位于中间的关键码,进行拆分),分裂后根节点与其子树均符合B-树的特性:
-
继续插入18、12、44,又一叶节点的关键码达到5个,继续分裂,中间元素被“上提”到父节点:
查找树的插入操作一般都会选择将新元素作为叶子节点插入,每次插入后,如果没有破坏B-树的规则,则插入结束;如果破坏了B-树的规则(插入操作中主要指关键码个数超出规定),则向进行分离操作,然后递归的检查父节点是否符合B-树的规则。
2.2 B-树的删除操作
一般来说,删除操作都会比插入操作更加的复杂,B-树也是如此。简单的来说B-树的删除可以分为以下几种情况:
-
被删除的关键码位于叶子节点,且删除后不破坏B-树规则,则直接删除;
-
被删除的关键码位于叶子节点,删除后该节点关键码个数小于ceil(M/2)-1,则根据其兄弟节点的关键码个数,执行借关键码(兄弟节点的关键码足够)或合并(兄弟节点的关键码不足)操作;
-
合并叶子节点后,父节点的关键码-1,相当于删除了内部节点的关键码,递归的执行删除逻辑即可;
-
被删除的关键码位于内部节点,则根据该关键码的左右子节点的关键码个数,执行借关键码(子节点的关键码足够)或合并(子节点的关键码不足)操作;类似的,合并后递归的向上执行删除逻辑。
继续以M = 5为例,首先构造了一颗元素“较多”的B-树,然后对其进行删除操作,被删除的元素被表示为红色:
-
构造了一颗5阶的B-树:
-
删除3,由于被删除的关键码存在于叶子节点,且删除后关键码个数符合B-树的规则,因此可以直接删除:
-
删除25,删除后该叶子节点的关键码数为1,小于ceil(M/2)-1,此处需要根据其兄弟节点是否有足够的关键码(大于等于ceil(M/2)),有则向兄弟节点借一个,没有则进行合并操作。显然该节点的右兄弟存在足够的关键码,因此只需要向兄弟节点“借“一个关键码即可(父节点下移一个关键码,兄弟节点上移一个关键码):
-
删除98,删除后面临和3中类似的情况,不同的是该叶子节点的兄弟节点没有足够的关键码可以借给它。此时,需要将该节点、该节点的兄弟节点以及父节点对应的关键码进行合并。合并操作会让父节点的关键码-1(相当于内部节点删除了一个元素),如果合并后父节点的关键码数不足,则递归的执行内部节点的删除操作:
-
删除31,被删除关键码所处的节点为内部节点,从内部节点中删除关键码首先需要观察“关键码对应的左右子节点”是否有足够的关键码,有则向子节点借一个,没有则进行合并。显然,此时31的右子节点有足够的关键码:
-
删除31,删除后面临和5中类似的情况,不同的是此时子节点没有足够的关键码,因此执行合并操作。合并后父节点关键码-1,递归的向上执行删除逻辑:
3. B+树
B+树是B-树的变体,也是一种多路搜索树,它与B-树类似,并规定了:
-
非叶子节点不保存指向数据的指针,只进行数据索引(简单的来说就是只存放关键码,不保存数据);
-
为所有叶子结点增加了一个链指针;
-
内部节点中一个关键码对应一个子节点(B-树中子节点个数比关键码个数多1),关键码是子节点中最大(或最小)的元素。
也就是说,B+树的数据都被保存在了叶子节点中,为什么要这么做呢?
回顾引入B-树的过程,选取B-树的初衷是为了降低树的高度,以减少磁盘I/0的次数。一般来说,树的每一个节点都对应着一个磁盘区块,节点中存储元素的大小限制了节点存储元素的个数,即影响了最终所获的树的高度,从而影响磁盘I/O的次数。 B-树中的每个节点都保存着关键码和指向数据的指针,它们共同构成了前面所说的“元素”,如果可以减小元素的大小,就可以进一步的降低树的高度。
由于多路查找树的增删查操作都依赖于关键码,因此关键码是必须保留的,但数据指针在查找过程中却不是必须的,因此可以只保存在叶子节点中。B+树就是遵循了此原理,它的内部节点只保存了关键码(以及子节点的索引),不保存数据指针,所有的数据都被保存在了叶子节点中:
-
使B+树获得更低的高度,从而减少磁盘I/O的次数;
-
由于数据都存储在叶子节点中,且叶子节点都位于同一层,所以查找操作是稳定的(每一次查找都需要相同的次数);
-
同时,因为所有叶子节点形成了一个有序链表,范围查询变得更加便利了。
下面给出了一个B+树了示例:
今天的文章B树 & B+树分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:http://bianchenghao.cn/19035.html