数据结构和算法学习笔记十五:多路查找树(B树)

时间:2023-11-12 23:37:56

一.概念

  1.多路查找树(multi-way search tree):所谓多路,即是指每个节点中存储的数据可以是多个,每个节点的子节点数也可以多于两个.使用多路查找树的意义在于有效降低树的深度,从而降低查找深度.

  2.2-3树:2-3树是指满足以下条件的多路查找树:1)每个节点可以是2节点(包含一个元素和2个子节点)或者3节点(包含两个元素和3个子节点);2)一个2节点要么没有子节点,要么有两个子节点,不存在只有一个子节点的情况;3)一个3节点同理,要么没有子节点,要么有3个子节点,且3节点的元素必须是两个.

  3.2-3-4树:相对于2-3树,还拓展了4节点(有3个元素和4个子节点),4节点同样需要满足条件:要么没有子节点,要么有4个子节点,其中的元素必须是3个.

  4.B树:这是一种平衡的多路查找树,2-3树和2-3-4树都是特殊的B树,其中最多子节点的节点的子节点数称为B树的阶.

  5.B+树:对B树的优化,在B+树中每个叶子节点还会记录下一个叶子节点的指针.B+树对B树的查找进行了优化,特别适合带有范围的查找,且插入和删除都必须在叶子节点上进行.

二.2-3树

  1.2-3树的存储规则

    1)我们以一个实际的2-3树为例,如下图所示:

数据结构和算法学习笔记十五:多路查找树(B树)

    我们可以看到,在这颗树中根节点8元素节点是2节点,2节点要么没有子树要么有两颗子树,2节点的存储方法和普通的二叉排序树相同,左子树中节点值都比当前节点的值小,右子树中的节点的值都比当前节点值要大;值得注意的是3节点,如图中的12\14元素节点,3节点要么没有子树,要么有3颗子树,且有3颗子树时同样要满足二叉排序树的定义,也就是说左子树中的节点值都比3节点中的最小值小(图中9\10的值比12小),中子树的所有节点的值都介于3节点中的两个值之间(图中13介于12和14之间),右子树中节点的值都比3节点中最大值要大(图中15比14大).最后需要注意,图中的所有叶子节点都在同一层,这也是2-3树存储时需要满足的规则.

    借助上图我们直观梳理了2-3树的存储规则,那么接下来我们看一下如何向2-3树中添加元素.

  2.二叉树的插入

    情况一:我们插入元素3,首先和根节点8比较,3比8小,向8的左子树插入,--->3比4小,向4的左子树插入,--->下面一个节点是一个叶子结点同时也是一个2节点,需要比较吗?显然不需要,因为如果将3元素节点作为1元素节点的子节点,不仅1节点只有一个子节点,不满足定义,而且所有叶子节点也不都在同一层,同样不满足定义.当插入时遇到叶子节点同时这个节点是2节点时,直接将这个2节点变为3节点即可,插入结果如下图所示:

数据结构和算法学习笔记十五:多路查找树(B树)

    情况二:我们继续再插入一个元素5,首先和8比较,5比8小,向8的左子树插入,--->5比4大,向4的右子树插入,--->插入到6\7元素节点中时出现了问题,这是一个3节点,无法插入,但是也不能将5作为6\7元素节点的子节点,原因自不用赘述,那么应该怎么办呢?我们可以这样做,先不管这是一个3节点,自顾自地将5插入其中,结果如下图所示:

数据结构和算法学习笔记十五:多路查找树(B树)

    插入后的5\6\7元素节点显然是不符合定义的,3个元素多了,我们需要拿出一个元素来形成一个新的节点,而且这个节点不能向下插入,怎么办呢?相信聪明的你已经想到了,向下不可以我们向上挪一个元素,4元素节点是一个2节点,多一个元素变成3节点也是可以的.但是这里又有了其他问题:哪个元素可以向上挪呢?4元素节点这个2节点变成3节点后需要有3颗子树,但是现在只有两颗子树,怎么办呢?第二个问题不难解决,现在的5\6\7元素节点有3个元素,挪动一个上去后将剩下的两个元素拆开,形成两个2节点即可,所以第一个问题也就解决了,一定是挪动5\6\7元素节点里面中间的元素6上去,因为3元素节点的中子树的所有元素值都要介于这个节点的两个值之间,不信的话你可以挪动5或7上去试一试.好了,现在我们经过对2-3树中部分节点的元素相互移动和节点拆分合并,成功将元素5插入了,结果如下图所示:

数据结构和算法学习笔记十五:多路查找树(B树)

    总结一下这次插入操作:元素插入时一定是插入叶子结点中的,当即将插入的节点是一个2节点时,直接插入即可,当要插入的节点是一个3节点时,先不管不顾地插入,然后将插入后节点的3个元素中中间的元素向上挪动,并将剩下的两个元素拆分成两个节点.

    诶,是不是又有问题了,如果挪上去发现上面的节点还是3节点怎么办呢?我们来看下面一个例子:

    情况三:插入元素11,显然经过比较(省略比较过程)应当插入9\10元素节点中,先插入,如下图所示:

数据结构和算法学习笔记十五:多路查找树(B树)

    接下来按照上一次的步骤我们将9\10\11这三个元素中中间的10元素向上挪,并将剩下的元素9和11拆开,结果如下图所示:

数据结构和算法学习笔记十五:多路查找树(B树)

    我们会发现,元素10挪上去时原来的12\14元素节点本来就是3节点,现在有3个元素了,然后将元素9和元素11拆开形成两个节点后,这个10\12\14节点就有4个子节点了(本来就有3个,又拆出来一个),那么接下来怎么办?还用问么,继续向上挪呗!将10/12/14这三个元素中的中间元素12继续向上挪,并将剩下的10和14拆开,就形成了两个2节点,诶,不是刚好有4个子节点吗?一切都是那么完美.继续挪动后形成了如下图所示的2-3树,11元素的插入就算完成了:

数据结构和算法学习笔记十五:多路查找树(B树)

    总结:如果你一直跟着我的思路读到这里的话,相信你已经发现了一些规律了.当我们向2-3树中插入元素时,一定是向叶子节点中插入的.如果这个叶子节点是2节点,直接插入将其变为3节点即可.如果这个叶子节点已经是3节点了,先插入,然后将3个元素中中间的元素挪给父节点并将剩下的两个元素拆分成两个节点,如果父节点是2节点的话,父节点就会变成3节点,如果父节点已经是3节点的话,同样先将挪上去的元素放入,然后将3个元素中中间的元素继续向父节点挪动并将剩下的两个元素拆分为两个节点,这个父节点刚好有4个子节点就可以分配给这两个2节点了...如果父节点的父节点还是3节点的话,不用说,继续向上挪动元素,直到遇到某个父节点是2节点停止或者到达根节点.很自然地我们会想到,如果一直挪到根节点并且根节点也刚好是3节点呢?有兴趣的话可以继续将元素2插入上图中的2-3树中试一试,就会出现这种情况.其实不难解决,如果将元素放入根节点中并将根节点此时的3个元素的中间元素向上挪动形成新的根节点,将剩下的两个元素拆分为两个节点刚好可以作为新根节点的子节点.其实这就是2-3树的深度增长的情况了.从这里可以看出来,2-3树的深度的增长不是向下增长的,而是向上增长的.

    总的来说,2-3树的插入方法可以用一个"挤"字总结,插入时直接插入节点中,如果节点中的元素个数达到3个,挤不下了,就将中间的元素向上挤并将剩下两个元素拆成两个节点,如果上一层的节点还挤不下,继续向上挤并将剩下的元素拆分......

  3.2-3树的删除实现

    2-3树的删除有很多形式,我们可以看一看下面的例子总结如何进行删除:

    情况一:删除下面2-3树中的节点6

数据结构和算法学习笔记十五:多路查找树(B树)

    我们可以发现,将6删除后4\6元素节点由3节点变成了2节点,那么子节点的个数必须减少1,我们很容易想到可以将5元素节点和7元素节点合并,结果如下图:

数据结构和算法学习笔记十五:多路查找树(B树)

    情况二:删除下面2-3树中的元素40:

数据结构和算法学习笔记十五:多路查找树(B树)

    显然,删除元素40后,原来的20\40元素节点由3节点变成了2节点,所以它的子节点的数目应该减少1,但是这里子节点都是3节点,显然不能合并,那么我们应该怎么办呢?我们可以从子节点中匀一个元素给这个节点,显然,只能匀35上去,匀完后就有了如下图的结果:

数据结构和算法学习笔记十五:多路查找树(B树)

    总结:我们可以在这里作一个小总结.将情况一和情况二对比,我们发现同样是3节点的一个元素被删除,情况一删除后变成了2节点,而情况二中删除后这个节点还是3节点,这是怎么回事呢?其实情况一也可以在移除完成后仍然是3节点的(读者可自行尝试看情况一能否保留3节点).这里我们用一个"借"字来总结这个移除过程:当某个元素被移除后,这个元素的位置就出现了一个空缺,那么如何去补这个空缺呢?我们可以通过借用子节点中的元素的方式来补,优先借用这个元素的相邻子节点中的元素,如情况一中的元素5和元素7,但是显然这两个元素都分别在2节点中,元素离开了这个节点就消失了,所以借不了,我们就考虑不借了,将剩下的元素合并保留4节点为2节点的形式;情况二中20\40元素节点中的元素40被移除,由于3个子节点都是3节点,所以直接借了子节点中的元素来用,当然如果子节点还有子节点,还需要继续借用元素来填补被借的元素位置或者合并元素.

    那么,如果我们遇到子节点的元素借不出去又合并不了的情况怎么处理呢?

    情况三:移除下面2-3图中的元素90:

数据结构和算法学习笔记十五:多路查找树(B树)

    显然,90元素所在节点是2节点,90元素移除后这个节点就没有元素了,必须从其他地方借用一个元素来填补或者合并节点.如果将85元素节点和95元素节点合并显然不是很合适,因为合并后将其作为80元素节点的右子节点就不满足2-3树的定义了,但是从85元素或者95元素中借用一个元素填补90元素的位置还是不满足2-3树定义.怎么办呢?是不是又想到了,子节点借不了向父节点借嘛,将80元素借用过来,然后80元素留下的空位再从它的左子树中借用一个元素来填补,首先考虑左子树中的最大元素43,这个元素被借用后又出现了空位,然后可以考虑将其父节点中的35元素借用过来填补空位,之后父元素留下的空位让25元素和35元素合并即可.结果如下图所示:

数据结构和算法学习笔记十五:多路查找树(B树)

    总结:我们可以看到,当一个元素被移除后,这个元素留下的空位我们可以采用借的方式来填补,首先从左右子树中去借,从子树中借一定借用的是左子树的最大元素或者右子树的最小元素.如果子树中没有元素可借,那么可以考虑合并子节点(子节点是叶子节点的话).当一个元素被借用来填补其他元素的位置,那么这个元素也可以采用同样的方案从它的子树中或者父节点中去借用元素,也可以考虑合并子节点.如果最终发现不能借用了(这种情况出现时,一定是借用了完全二叉树中的节点),这时就要考虑将树的深度降低1了(这里不再赘述如何降低树的深度).

三.2-3-4树

  对于2-3-4树,这里可以不再赘述,因为2-3-4树就是2-3树的进一步拓展,其添加元素或者删除元素的过程和2-3树相同.

四.B树:

  B树是平衡的多路查找树,2-3树和2-3-4树都是B树的特例.

五.B+树:

  我们在说到2-3树时有讲到在2-3树中所有叶子节点都在同一层,这也是所有B树都需要满足的条件.如果我们在这些叶子节点中增加两个指针,指向父节点中的相应元素及下一个兄弟节点的位置,那么这样的树就是B+树.B+树特别适合有范围的查找(根据叶子结点中下一个叶子节点的指针遍历所有叶子节点,根据父节点的指针判断范围).