在大规模数据存储中,二叉查找树的深度会过大,当内存无法存储所有节点数据时,需要读取磁盘,进行IO操作,从而树的高度越高,I/O操作次数越多,效率也就越低。所以诸如之前所讲的红黑树,AVL树 因为树的高度太高而不适合这种需要大量IO操作的查询。所以,B树通过多叉的实现来降低树的高度,从而减少IO操作的次数。
为方便描述,下面一律用B树这个名称。B树是一种多路平衡搜索树(非二叉),若其是M路,则:
B树与二叉搜索树的最大区别在于其每个节点可以存不止一个键值,并且其子女不止两个,不过还是需要满足键值数=子女数-1。因此,对于相同数量的键值,B树比二叉搜索树要更加矮一些,特别是当M较大时,树高会更低。
上图中是一个简单的B树,在实际应用中,M可以取到很大,比如大于1000。一般情况下M的取值会使得每个磁盘盘块可以正好存放一个B数节点。上图中的35节点,35是一个key(或者说是索引,比如磁盘文件的文件名),而小黑块则代表的是该key所指向的内容在磁盘中实际的存储位置,是一个指针(比如35这个文件在硬盘中的位置)。
B树的搜索与二叉搜索树类似,只不过需要在节点内部进行一次搜索查找。从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点;
B树的插入首先查找插入所在的节点,若该节点未满,插入即可,若该节点以及满了,则需要将该节点分裂,并将该节点的中间的元素移动到父节点上,若父节点未满,则结束,若父节点也满了,则需要继续分裂父节点,如此不断向上,直到根节点,如果根节点也满了,则分裂根节点,从而树的高度+1。
下面是B树插入的一个演示动画,往B树中一次插入的元素为6 10 4 14 5 11 15 3 2 12 1 7 8 8 6 3 6 21 5 15 15 6 32 23 45 65 7 8 6 5 4。
B树的删除首先要找到删除的节点,并删除节点中的元素,如果删除的元素有左右孩子,则上移左孩子最右节点或右孩子最左节点到父节点,若没有左右孩子,则直接删除。删除后,若某节点中元素数目不符合B树要求(小于M/2-1取上整),则需要看起相邻的兄弟节点是否有多余的元素,若有,则可以向父节点借一个元素,然后将最丰满的相邻兄弟结点中上移最后或最前一个元素到父节点中(有点类似于左旋)。若其相邻兄弟节点没有多余的元素,则与其兄弟节点合并成一个节点,此时也需要将父节点中的一个元素一起合并。
B+树是B树的一个变种,其也是一种多路平衡搜索树,其与B树的主要区别是:
B+树主要是应文件系统所需而产生的。文件系统中,文件的目录是一级一级索引,只有最底层的叶子节点(文件)保存数据。非叶子节点只保存索引,不保存实际的数据,数据都保存在叶子节点中,所有的非叶子节点都可以看成是索引部分。
非叶子节点(比如[5,28,65])只是一个key(索引,实际的数据在叶子节点上,对应于叶子节点[5,8,9]中的5,[28,30,33]中的28,[65,73,79]中的65才是真正的数据或指向真实数据的指针)。
B+的搜索与B树也是基本相同的。唯一的区别是B+树只有达到叶子结点才命中,因为只有叶节点中存放着真实数据或真实数据的指正,而B树可以在非叶子结点命中,其性能也等价于在元素全集做一次二分查找。
B+树的插入与B树类似,如果节点中有多余的空间放入元素,则直接插入即可。如果节点本来就已经满了,则将其分裂为两个节点,并将其中间元素的索引放入到父节点中,在这里如果是叶子节点的话,是拷贝中间元素的索引到父节点中(因为叶子节点需要包含所有的元素),而如果是非叶子节点,则是上移节点的中间元素到父节点中。
在叶节点中删除元素,如果节点还满足B+树的要求,则okay。如果元素个数过少,并且其邻近兄弟节点有多余的元素,则从邻近兄弟节点中借一个元素,并修改父节点中的索引使其满足新的划分。如果其邻近兄弟节点也没有多余的元素,则将其和邻近兄弟节点合并,并且我们需要修改其父节点的索引以满足新的划分。并且如果父节点的索引元素太少不满足要求,则需要继续看起兄弟节点是否多余,如果没有多余则还需要与兄弟节点合并,如此不断向上,直到根节点。如果根节点中元素也被删除,则把根节点删除,并由合并来的节点作为新的根节点,树的高度减1。
B+树的非叶子节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,盘块所能容纳的关键字数量也越多,具有更好的空间局部性,一次性读入内存的需要查找的关键字也越多,相对的IO读写次数也就降低了。
另外对于B+树来说,因为非叶子节点只是叶子节点中关键字的索引,所以任何关键字的查找都必须走一条从根节点到叶子节点的路,所有关键字查询的路径长度相同。而若经常访问的元素离根节点很近,则B树访问更迅速,因为其不一定要到叶子节点。
数据库索引采用B+树的主要原因是B树在提高了IO性能的同时并没有解决元素遍历效率低下的问题,而也正是为了解决该问题,B+树应运而生。因为叶子节点中增加了一个链指针,B+树只需要取遍历叶子节点可以实现整棵树的遍历。而且数据库中基于范围的查询是非常频繁的,B树对基于范围的查询效率太低。
B*树又是B+树的变种,其与B+树的区别有:
B树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;B树分配新结点的概率比B+树要低,空间使用率更高;