大家好。数据结构与算法这一讲
我们继续第十一章索引的学习。今天我们主要是学习
动态索引其中的B树。 那我们动态索引呢主要是针对
这个主关键码它经常进行插入和删除这样的动态更新的情况。
那我们前面提到的这样的静态多分树呢它就不是非常好调整,也不好维护。
那因此呢我们就要设计一种新的数据结构
来支持这样的操作,这就是我们的B树的来由。
那我们来看这个B树呢它其实很像
BST树。那这就是一个三阶的B树。
它的特点呢就是子结点个数在两到三个
之间,它不能有一个的那样的子结点的情况。
另外呢所有的叶结点都在同一层。在中序的遍历下呢,它是一个有序的这样的序列。
那我们来看一下B树的定义。
那我们为了适应这样的结点的插入删除的动态性,
那我就使得一个结点呢它可以也是在一个全满到半满之间的范围浮动。
因此呢我们会定义结点的这样的一个阶。那首先这个阶呢,
它还受制于我们这个磁盘空间的大小。
那也就是说我们一个磁盘页框,如果是它只
能够容纳比如说512子结或者是1024子结,
那我们这样的一个结点呢我就只能够容纳
有限的关键码以及指针这样的对。
因此呢我们可以根据这样的一个磁盘页的大小限制计算出
我这个B树它最大的阶的个数是什么样的。
那因此呢我们首先就是得到一个这个结点它的阶。 然后呢我们再规定
它全满是不超过m个子结点,然后半满呢我们允许它是
二分之m个子结点。对于根我们有一个特别的处理,
就是说根呢它可以只有两个子结点。这个我们在后面可以看到,
就是因为我们在动态调整的时候对于根有一些特殊的情况。
还有一个限制呢就是所有的叶结点我们都要在同一层。这就
使得它有一个比较好的一个性能,通过这个限制叶结点在同一层呢
达到了一种平衡使得它的树高整体是一个log m级的。
还有一个呢就是对于那个有k个子结点的内部结点,
那它的关键码呢,它是k减一个。
那这就类似于BST。那BST呢,它如果是有两
个子结点的话呢,那它中间有一个分界的这个关键码。
那我们看这个树呢它正好是这样的一个情况。 那我们再来分析一下,
B树的性质。首先它的高度是平衡的,所有叶都在同一层。这样
使得B树呢它是在一个log以m为底
N的对数这样的一个量级。
然后它的关键码呢我们也是不允许重复的。对于
数据结构里面很多这种索引的情况我们都不让关键码重复。
如果你在运用中实在是需要重复,那你可以做一些特别的一些处理。
还有就是B树呢它把这些相关的这些记录呢
它其实是放在同一个磁盘页框。也就是说
B树里面这样一个结点,它们的这个关键码
是在同一个磁盘页框存储的。当然还
包括指向它的下一级结点的这样的指针。
那这就使得我们一些相关的关键码呢在相连的区域存放,
这使得我们能够很好的利用这样内存的这种缓冲,那我可以
在访问数据的时候预取一些相应的磁盘块放在内存缓冲区域里面
加快我们的这样的检索的速度。B树它还有一个特别重要的性质
就是它的页框是在全满到半满之间。
这就使得它的插入删除的这个性能呢是能得到一定的保证。
如果我们在一个半满到全满的这么一个范围之内呢,那如果
要插入的时候,如果它还没有全满我就有空间可以给它塞进去。
那删除的时候呢,那因为它也不要求你是给它挤得满满当当的,
那如果我没有下一处,只要删到这个举步那我整个树呢
也许不需要进行很大的调整。我们再后面一些事例的时候大家就可以看出来。
那我们一个B树的结点它其实是这么
一个结构。这是一个有j加一个子结点的一个B树结点。
那它就有j个关键码,P0到Pj呢它是分别
指向这个j加一个这样的子结点的一个结构指针。
K1到Kj呢它其实就是这些子结点的
分界的关键码。那也就是说P1指向的整个这个
子树的关键码都会小于K1,然后呢
在小于这个P1指向的这个子树,那一直这样。那其实就有点像我们
在中序下遍历BST它是一个有序的这样的序列。
那我们再想一想,除了这个P0到Pj,
那还有一些什么指针呢?其实还是有的。
那就回归到我们对于检索和索引的一些约定。
通过这个K找到它的value,这个value呢
有可能并不一定就是存在这个索引结构里的,
它是存在某一个原来你放数据的这个主文件,或者是其它的地方。
那因此呢,我们找到这个key以后,我要通过key 它所对应的一个这样的隐含指针,
再找到这个key在这个主文件里面它所拥有
的相应的那些数据信息也可以说是属性信息。
那我们来看一下这个图式。那这一个呢就是一个B树的这个索引。
那我们这些线呢,其实就对应到前面那个p,就是子结点的这个结构指针。
然后呢我们所有的这些A呢,其实就是对应到我们
这个关键码它的文件页里面的那个地址的指针。 每一个这样的key它都对应到
主文件里面具体的一个地址。那这个地址呢我们就可以
读出相应的一些数据信息。
B树我们要注意呢,阶一般是应该是大于等于三呢, 可以说阶最小的B树呢就是二三树。
那这个二三树其实在内存里面它也是一种非常好的一个
索引结构,因为它树高也是平衡的,而且它的实现非常地方便。
那其实B树在内存里面我们也可以用来组织索引结构。
它比这个BST呢就是也是能有更好的性能。
那我们来看一下B树的检索。
检索它其实是分成两个
步骤。这个步骤呢就是我们先从这个根呢
来读出这个相应的这个关键码。因为B树的阶可以很大,那它是有
j个这样的关键码,然后有j加一个这样的指针的话,那我们在这个
阶的关键码里面我也许可以用二分,因为它也不是很多,顺序也行,
那我们找到它的这个关键码是不是在根里面存在,如果存在我就可以停止。如果不存在呢,
那我就需要通过这个它的关键码这个分界的性质
去继续到下一层来查找,一直找到
相应的关键码为止。或者是说我们找不到,
那其实就是这个下一层关键码呢我们就会指向的
这个外部的这个空结点其实我们在失败的检索情况下 都是停在这样的一个叶层
这一个位置,然后它再往下索引的时候,已经是没有外部结点了,那就是这个查找失败。
插入的过程其实是一个失败的检索,也就是说我们要
检这个关键码,如果检到了那我告诉它B树不允许插入重复的关键码。
那如果是失败了,那我们一定是
停在叶层,那我就是在这个叶层的相应位置呢, 我就插入关键码。
那插入的过程我们一定要注意保持B树的性质。
那这个B树的性质它主要是有两个方面的约束。一个就是说它的这个
阶的约束,这个阶呢,对于子结点数
其实也对于关键码的个数我们就是要保持它 不能够溢出,因为是插入
那如果是上一处呢,那怎么办呢,我一个磁盘块,我是放不下这么多东西的
那我就必须把它分成两块,
在分裂的过程,我需要把它的分解关键码呢给他提出来
放到上一集有插入在上一集有一个新的分阶关键码,
那这样话,上一层它也可能会产生上一处,一直这样传下去
也许会传到根。那我们来看这个插入的实例。这是一个3阶的B树,
我们插入14,那这个插入的过程我就是顺着这个根结点然后
往下面一直找。那我找到这个叶这一层,然后我可以
插到这个接点里面,那插入以后,这个是正好它没有发生树的一些形状的改变,
那我们就完成了这个插入。
那继续插入55, 那我们找到这一个结点。
找到这个结点以后我们插入的时候呢因为这是一个
3阶B树,那它就已经是上一处喇,上一处了以后呢我们就
需要把它分成两块,而且它的中间呢个中位数52呢就作为这个新的
这两块新的关键码插到在上一层。
然后呢,这一次的插入操作就停止了。
继续插入19,那我们可以看到跟刚才类似。
那这个是上了处了以后,它的分解要往上面传。
这上一层,它还是接着上一处,
那他们又是产生两个新结点,然后它的分界的这个关键码呢
就传到根,然后根呢也上一处了,所以我们产生这个结点的分裂
然后呢我们形成了一个新的根结点。
这个根结点只像刚才分裂的这个老的根结点和它的复制的这个结点。
从这里可以看出在根结点为什么对它有特别的这个阶的规定也就是说
它的这个下阶呢是M等于2。这
就是因为呢我们在这样的一个插入过程中当我分裂到根的时候,
如果你这个M的阶很大比如说M等于100, 那我如果还要求满足它的这样一个下阶,就是它的
这个直接能够数是2份之M,也就是说50这种情况
那处它到那处去找这么多结点来筹备根呢?是不好找的。
那我们为了方便这个运算,我就是允许根结点作为
一个特例,它可以只有两个枝结点。
那我们来总结一下,刚才这个连续的插入过程它们会
有那一些访外的这样的操作,因为我们知道对于这个外层的这样的操作的话,访外是非常的
耗时间的,那我们所有的这些技术都是为了要减少访外次数。
所以我们要理解
它到底有那些访外次数。那我们来看刚才连续插入14,55,19 这样的过程,
假设我们在访问的过程中呢这些制版叶块
它都还放在内存的缓冲区里面
也就是说我下一次要继续访问这个磁盘块
的时候呢我不需要去访外。
那我们在插入14的过程中呢,我们会是源着 A,
B, F访问这样的读盘的操作,然后接着在插入55的时候呢是A,
D, K, 但是A呢已经是在缓冲区里面所以
这一次呢它不需要访外,直接从缓冲区拿出来就行了。
接着呢是插入19,那就是A,C, G,也是只要我访问C和G
所以这一个一起是有7次读盘的操作,完成了这样的插入以后呢
它的结果其实本质上都是
需要写回外层的。当然在一些数据库的技术里面呢,它可以比如说
延缓写出,不是一定马上写出。
但是呢如果我们假设它是马上写出的话呢
或者说我带缓冲情况下的这个写出也就是说
原来这个根结点以及
我们最后形成的这个新的根结点也许我们不是说插入完14就全都给他写出,但是我们把
必要的都要写出的话那这种情况下, 那我们写出那一些结点呢就是f
把它更改一下就行了, 然后呢插入55的时候它会要形成两个
新的结点就是K和K一撇,对这个D呢也要进行一个
修改的这个操作,插入19的时候呢,它对这个G有分裂, C也有
A也有。那就是G, G一撇, C, C一撇, A, A一撇。
以及A, A一撇,它们的一个新的根的赋值点就是T,那
因此我们总共呢有11次写盘的操作。
B树的删除,也就是一次成功的检索的基础上
然后再进行删除。有两种情况,一种就是关键码它如果不是在
叶结点的时候呢,那我们还是要跟它
在叶结点层里面的后继或者是叶结点层那里面的它的那个前区
来进行交换,然后呢把这个操作呢给它降到叶结点开始 进行。因此我们
下面呢就统一就是先考虑这个叶结点的
某一个值的一个删除。
那删除了以后,我们主要考虑的就是它会不会造成一个下叶初
不满足我们b处这个平衡性能的要求
那如果删除以后它不小于这个2份M减1,那它就没有产生上一处,那
我们就直接删除,如果是删除以后
关键码的格数已经是产生下一处了,那我们就需要进行特殊处理.
在删除的情况下,我们是首先考虑像它的这个
左右邻居借关键码,注意这个左右邻居
一定是亲兄弟,也就是说它们是共同拥有一个直接的父节点。
不能去接堂兄弟的那些值。
如果是亲兄弟某一个它能够给他借结点也就是说 亲兄弟它并没有处于哪个邻接的情况下,也就是
没有在这个允许的这个最小的关键码码树这个情况,那它就可以两个结点呢以及
它们在父节点里面的那样的分解的关键码
它们一起把这个新的这个中位数传到父结点
做一个新的分解,那么成两部分的这些关键码值。
如果是两个兄弟它都不能够借关键码这种情况呢
它就必须跟其中的一个进行合并。
那我们来看一下删除的示例,
现在呢是一个5阶的B树,
删除这个关键码92,那92呢我们找到它的一个后继,
就是94, 那调换以后呢把92给它挪到叶结点里面我就可以删除了。
那删除了以后呢,我们发现h这个结点呢它下一处了就是
5阶的B树它最小的关键码树呢必须是
有两个。那现在下一处以后呢,它一看这个左邻居还可以借关键码。
那把这个左邻居以至它自己还有在分结里面的那些关键码呢,
我们把它排在一起然后把这个中位数90呢,给提上去那我们就
已经完成了这个删除的操作。
继续在树里面我们删除96, 那还是这样我们跟它的后继呢
交换以后,然后删除96, 删除了96以后,这个i结点就发生了
下一处,但是它想向h借的时候呢,h也是一个邻接的情况,
所以我们就把h和i这两个结点呢给它合并,
而且不要忘了它们在父节点里面的那个分结
的关键码也要拿进来合并,也就是说父节点里面要删掉一个这个
关键码。删掉以后呢, 父节点它也是下一处了,它就要去看一下
它的这个兄弟是不是可以借,如果不可以借,
那这时候我们只能继续合并。那这时候, b,c以至它们这父节点里面
的那个分节一起合并, 那合并以后呢我们整个这个b树呢它就降低了
一层了。那我们看一下这个b树的性能。
首先我们就是先估计一下这b树它的高度会是怎么样的。
最高的b树呢就是假设每一层它这个结点都是在这个就下阶的这样的一个关键码值的一个状况,
因此它能够形成最高的b树。
那我们首先呢来看,如果我是有N个关键码,看到这个B树里面呢
也可以看到我们这种画红色的就是表示它是这个外部结点,
是我们在这一个叶结点这一层,那如果我们检索
还要去检别的值,比如说要检零三六这样的一个值,
那它呢就会是落在零零
八和零四零这个中间的
那一个外部结点。然后它检它也是个空,我们也不能继续下去,所以它就会是
返回检索失败。对于N个关键码的这样的B树,它总共呢有N加一个外部的这个空结点。
那我们假设我们的这个层的这样的一个编号呢就是根是第零层。
那我们在这个叶层呢是可以减一层,那
这个外部呢是第K层。那我们来看一下各层的这个结点数。
那根底下它的函子最少的情况下就是只有两个指结点。第二层呢,那我们就是二乘以
二分之m个指结点。那我就是尽量的每一层都是
关键码指呢是尽量的少,让这个数呢尽量的高。
那我们一直到第k层的时候呢,它会是二乘以
二分之m,然后可以减一次方这样多个指结点。
那我们就能够得到这个N加一它是大于等于
这个我们刚计算出来的第k层的这个指结点数。得到这个k
它的这么一个值肯定是
小于等于一加log二分之m为底的
二分之N加一的对数。那这个值你可以看到它是log以
m为底然后N的一个对数这样的量级的。我们假设N是
两百万左右的这样的规模,然后对于一定的这样的磁盘空间
的限制的话,我假设算出来这个m是一百九十九。
那可以看到呢,这个k呢它其实就是四,也就是说
顶多四层,我就能够把它给索引起来。那因为这个
根,还有根底下的这一层的话就总共加起来你可能也就是
两百个结点,那这一些结点我可以把它存在累存里面。
因此我真正需要反外的只是就是最底下的这两层。
所以B树的索引效率是非常高的。
那我们再来看这个B树结点分裂的次数。 如果是从一个空的B树开始,
逐个逐个的关键码插入,然后当然那个最开始的这个根结点
我们假设它不是经过分裂的,是我先给你的。那一部结点数是我假设是p的话,
那我们大概可以得出N和这个p的一个关系。
那我们如果是每一个都是经过分裂而来的呢,那我们可以得到这个那一部关键码
p和这个N的一个关系。那我们如果是平均到每一个关键码
那它没插入的时候它可能被分裂的
这么一个概率,我们就是p减一除以N。
那可以看到呢是跟这个m是相关的。这个概率呢其实如果我们把
刚才那m是等于一百九十九,放进来的话那其实也就是百分之一左右
的这个情况它需要分裂,大部分的情况下它其实是不需要分裂的。
那我们再给出几个思考。首先
呢就是说,你想一想,如果有二阶的B树会是什么样?
第二个问题呢就是说,我们在B树的删除里面
我们都是先去借再合并,也就是说我尽量
少发生这种结构上的这个大变化, 但是你看它的插入的时候呢是先给它分裂,
然后我根本不考虑说还把我的这个多余的关键码送给
邻居的情况。那就是为什么呢?因为呢就是我们B树的定义
都是在二分之m到m之间的这样的一个度。
那我是不是可以有其他的这个划分呢,比如说我可不可以是三分之
m到m这样的一个区域呢?那你可以比较一下
这两种不同的方法,它有一些什么不同你在算法上在写的时候会有一些什么差异。
另外呢就是这个B树呢它其实真的是一个比较高效的而且很方便实现的
这么一个平衡的索引数。如果是对于这个内存的数域
它特别大的时候,然后你又要考虑它的一个平衡的性呢
你用B树的这种变体来写这样的一个索引结构也是可以的。而且在这种情况下,
你不需要考虑它的这个结的限制呢,只要你觉得
在内存里面是一个合适的结就可以。它不受这个外层的磁盘空间大小的限制。
谢谢大家。