搜索树之B-/B+树的特性与实现

现代计算机中,在内存与外存(磁盘)组成的二级存储系统中, 数据全集往往存放于外存中,计算过程中则可将内存作为外存的高速缓存,存放最常用数据项的复本。借助高效的调度算法,如此便可将内存的“高速度”与外存的“大容量”结合起来。
两个相邻存储级别之间的数据传输,统称I/O操作。各级存储器的访问速度相差悬殊,故应 尽可能地减少I/O操作。仍以内存与磁盘为例,其单次访问延迟大致分别在纳秒(ns)和毫秒(ms) 级别,相差5至6个数量级。也就是说,对内存而言的一秒/一天,相当于磁盘的一星期/两千年。 因此,为减少对外存的一次访问,我们宁愿访问内存百次、千次甚至万次。也正因为此,在衡量相关算法的性能时,基本可以忽略对内存的访问,转而更多地关注对外存的访问次数。

多路搜索树

当数据规模大到内存已不足以容纳时,常规平衡二叉搜索树的效率将大打折扣。其原因在于,查找过程对外存的访问次数过多。
例如,若将10^9个记录在外存中组织为AVL树,则每次查找大 致需做30次外存访问。那么,如何才能有效减少外存操作呢?
为此,需要充分利用磁盘之类外部存储器的另一特性: 就时间成本而言,读取物理地址连续 的一千个字节,与读取单个字节几乎没有区别。
既然外部存储器更适宜于批量式访问,不妨通过 时间成本相对极低的多次内存操作,来替代时间成本相对极高的单次外存操作。相应地,需要将 通常的二叉搜索树,改造为多路搜索树—在中序遍历的意义下,这也是一种等价变换。

《搜索树之B-/B+树的特性与实现》

具体地如上图所示,比如可以两层为间隔,将各节点与其左、右孩子合并为“大节点”:

  • 原节点及其孩子的共三个关键码予以保留;
  • 孩子节点原有的四个分支也予以保留并按中序遍历次序排列;
  • 节点到左、右孩子的分支转化为“大节点”内部的搜索,在图中表示为水平分支。

如此改造之后,每个“大节点”拥有四个分支,故称作四路搜索树。

这一策略还可进一步推广,比如以三层为间隔,将各节点及其两个孩子、四个孙子合并为含 有七个关键码、八个分支的“大节点”,进而得到八路搜索树。一般地,以k层为间隔如此重组, 可将二叉搜索树转化为等价的2^k路搜索树,统称多路搜索树(multi-way search tree),亦趁为m(m>=2)路平衡搜索树。

多路搜索树同样支持查找等操作,且效果与原二叉搜索树完全等同;
然而重要的是,其对外存的访问方式已发生本质变化。实际上,在此时的搜索每下降一层,都以“大节点” 为单位从外存读取一组(而不再是单个)关键码。更为重要的是,这组关键码在逻辑上与物理上都彼此相邻,故可以批量方式从外存一次性读出,且所需时间与读取单个关键码几乎一样。

《搜索树之B-/B+树的特性与实现》

B-树癿宏观结极(外部节点以深色示意,深度完全一致,且都同处最底层)

其中,所有外部节点均深度相等。同时,每个内部节点都存有不超过m – 1个关键码,以及 用以指示对应分支的不超过m个引用。具体地,存有n =< m – 1个关键码 和有n + 1 =< m个引用。

反过来,各内部节点的分支数也不能太少。
除根以外的所有内部节点,都应满足: n+1 >= m/2
而在非空的B-树中,根节点应满足: n+1 >= 2

由于各节点的分支数介于m/2至m之间,故m阶B-树也称作(m/2, m)-树,如(2, 3)-树、
(3, 6)-树或(7, 13)-树等。

《搜索树之B-/B+树的特性与实现》

如上图a所示,即为一棵由9个内部节点、15个外部节点以及14个关键码组成的4阶B-树, 其高度h = 3,其中每个节点包含1~3个关键码,拥有2~4个分支。

作为与二叉搜索树等价的“扁平化”版本,B-树的宽度(亦即最底层外部节点的数目)往 往远大于其高度。因此在以图形描述B-树的逻辑结构时,我们往往需要简化其中分支的画法,并 转而采用如图b所示的紧凑形式。

另外,既然外部节点均同处于最底层,且深度完全一致,故在将它们省略之后,通常还不致 造成误解。因此,还可以将B-树的逻辑结构,进一步精简为如图(c)所示的最紧凑形式。

关键码查找

如前述,B-树结构非常适宜于在相对更小的内存中,实现对大规模数据的高效操作。 一般可以将大数据集组织为B-树并存放于外存。

对于活跃的B-树,其根节点会常驻于内存;此外,任何时刻通常只有另一节点(称作当前节点)留驻于内存。 B-树的查找过程,与二叉搜索树的查找过程基本类似。

首先以根节点作为当前节点,然后再逐层深入。若在当前节点(所包含的一组关键码)中能
够找到目标关键码,则成功返回。否则(在当前节点中查找“失败”),则必可在当前节点中确 定某一个引用(“失败”位置),并通过它转至逻辑上处于下一层的另一节点。若该节点不是外 部节点,则将其载入内存,并更新为当前节点,然后继续重复上述过程。
整个过程如下图所示,从根节点开始,通过关键码的比较不断深入至下一层,直到某一
关键码命中(查找成功),或者到达某一外部节点(查找失败)。

《搜索树之B-/B+树的特性与实现》

与二叉搜索树的不同之处在于,因此时各节点内通常都包含多个关键码,故有可能需要经过 (在内存中的)多次比较,才能确定应该转向下一层的哪个节点并继续查找。

仍以上图包含:[7]、[19, 22]、[28]、[37, 40, 41]、[46]和[52]的外部节点为例。查找关键码41的过程大致如下:

  • 在根节点处经过一次关 键码比较(25)之后,即可确定应转入第2个分支;
  • 再经过两次比较(34, 43)之后,确定转入第2个分支;
  • 最后经过三次比较(37, 40, 41)之后,才成功地找到目标关键码

查找关键码42的过程与之类似,只是在最底层的内部节点内,需要经过三次关键码比较(37, 40, 41)之后,才确定应转入关键码41右侧的外部节点,从而最终确定查找失败。

可见,只有在切换和更新当前节点时才会发生I/O操作,而在同一节点内部的查找则完全在 内存中进行。因内存的访问速度远远高于外存,再考虑到各节点所含关键码数量通常在128~512 之间,故可直接使用顺序查找策略,而不必采用二分查找之类的复杂策略。

多路搜索树的插入,删除

在B-树中插入一个新的关键码e,首先在树中查找该关键码。若查找成功, 则按照“禁止重复关键码”的约定不予插入。否则,查找过程必然终止于某一外部节点v,且其父节点由变量 _hot指示。当然,此时的_hot必然指向某一叶节点(可能同时也是根节点)。接下来,在该节 点中再次查找目标关键码e。尽管这次查找注定失败,却可以确定e在其中的正确插入位置r。最后,只需将e插至这一位置。
至此,_hot所指的节点中增加了一个关键码。若该节点内关键码的总数依然合法(即不超过m – 1个),则插入操作随即完成。否则,称该节点发生了一次上溢(overflow)

在B-树中删除关键码e,也首先需要在树中查找e所属的节点。倘若查找失败, 则说明关键码e尚不存在,删除操作即告完成。否则,目标关键码所在的节点必由返回的位置v指示。此时,通过顺序查找,即可进一步确定e在节点v中的下标。

假定v是叶节点,直接将e(及其左侧的外部空节点)从v中删去。如此,节点v中所含的关键码以及(空)分支将分别减少一个。
如v不是叶节点,e的直接前驱(后继)在其左(右)子树中必然存在,而且可在O(height(v))时间内确定它们的位置,其中height(v)为节点v的高度。此处不妨选用直接后继。于是,e的直接后继关键码所属的节点u必为叶节点,且该关键码就是其中的最小者u[0]。 既然如此,只要令e与u[0]互换位置,即可确保待删除的关键码e所属的节点v是叶节点。此时,重复上述过程即可完成删除。

此时,若该节点所含关键码的总数依然合法(即不少于m/2 – 1),则删除操作随即完成。 否则,称该节点发生了下溢(underflow)

上溢与分裂

一般地,刚发生上溢的节点,应恰好含有m个关键码。若取s = m/2,则它们依次为:
{ k0, …, ks-1; ks; ks+1, …, km-1 }

可见,以ks为界,可将该节点分前、后两个子节点,且二者大致等长。于是,可令关键码ks上升一层,归入其父节点(若存在)中的适当位置,并分别以这两个子节点作为其左、右孩子。 这一过程,称作节点的分裂(split)。

不难验证,如此分裂所得的两个孩子节点,均符合m阶B-树关于节点分支数的条件。

以如下图所示的6阶B-树局部为例,其中节点{ 17, 20, 31, 37, 41, 56 },因
所含关键码增至6个而发生上溢。
为完成修复,可以关键码37为界,将该节点分裂为{ 17, 20, 31 } 和{ 41, 56 };
关键码37则上升一层,并以分裂出来的两个子节点作为左、右孩子。

《搜索树之B-/B+树的特性与实现》

被提升的关键码,可能有三种进一步的处置方式。首先如图(a1)所示,设原上溢节点的父
节点存在,且足以接纳一个关键码。此种情况下,只需将被提升的关键码(37)按次序插入父节 点中,修复即告完成,修复后的局部如图(a2)所示。

其次如图(b1)所示,尽管上溢节点的父节点存在,但业已处于饱和状态。此时如图(b2), 在强行将被提升的关键码插入父节点之后,尽管上溢节点也可得到修复,却会导致其父节点继而 发生上溢这种现象称作上溢的向上传递。好在每经过一次这样的修复,上溢节点的高度都必 然上升一层。这意味着上溢的传递不至于没有尽头,最远不至超过树根。

最后如图(c1)所示,若上溢果真传递至根节点,则可令被提升的关键码(37)自成一个节点,并作为新的树根。于是如图(c2)所示,至此上溢修复完毕,全树增高一层。可见,整个过程中所做分裂操作的次数,必不超过全树的高度

下溢与合并

由上,在m阶B-树中,刚发生下溢的节点V必恰好包含m/2 – 2个关键码和m/2 – 1个分 支。以下将根据其左、右兄弟所含关键码的数目,分三种情况做相应的处置。

V的左兄弟L存在,且至少包含m/2个关键码
《搜索树之B-/B+树的特性与实现》

如上图(a)所示,不妨设L和V分别是其父节点P中关键码y的左、右孩子,L中最大关键码为x(x < y)。此时可如图(b)所示,将y从节点P转移至节点V中(作为最小关键码),再将x 从L转移至P中(取代原关键码y)。至此,局部乃至整树都重新满足B-树条件,下溢修复完毕。

V的右兄弟R存在,且至少包含m/2个关键码
《搜索树之B-/B+树的特性与实现》

V的左、右兄弟L和R或者不存在,或者其包含的关键码均不足m/2个

实际上,此时的L和R不可能同时不存在。如下图a所示,不失一般性地设左兄弟节点L
存在。当然,此时节点L应恰好包含m/2 – 1个关键码。

《搜索树之B-/B+树的特性与实现》

于是为修复节点V的下溢缺陷,可如上图(b)所示,从父节点P中抽出介于L和V之间的关键码y, 并通过该关键码将节点L和V“粘接”成一个节点—这一过程称作节点的合并(merge)。注意, 在经如此合并而得新节点中,关键码依然合法。

接下来,还须检查父节点P–关键码y的删除可能致使该节点出现下溢。好在,即便如此, 也尽可套用上述三种方法继续修复节点P。当然,修复之后仍可能导致祖父节点以及更高层节点 的下溢—这种现象称作下溢的传递。特别地,当下溢传递至根节点且其中不再含有任何关键码 时,即可将其删除并代之以其唯一的孩子节点,全树高度也随之下降一层。

B+树是应文件系统所需而产生的一种B-树的变形树。一棵m阶的B+树和m阶的B-
树的差异在于:

  • 有n 棵子树的结点中含有n 个关键码;
  • 所有的叶子结点中包含了全部关键码的信息,及指向含有这些关键码记录的指针,且叶子结点本身依关键码的大小自小而大的顺序链接。
  • 所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键码。

《搜索树之B-/B+树的特性与实现》

通常在B+树上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点。因此可以对B+树进行两种查找运算:一种是从最小关键字起顺序查找,另一种是从根节点开始,进行随机查找。

在B+树上进行随机查找、插入和删除的过程基本上与B-树类似。只是在查找时,若非终端结点上的关键码等于给定值,并不终止,而是继续向下直到叶子结点。因此,在B+
树,不管查找成功与否,每次查找都是走了一条从根到叶子结点的路径。

    原文作者:B树
    原文地址: https://blog.csdn.net/canot/article/details/79999164
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞