前面提到的平衡二叉树用于查找十分方便,但是这种树只能用于数据量较小的场景。当数据集较大,比如有几百万行记录的文件,其容量超过了内存的大小,就需要储存在磁盘上。在这种时候,平衡二叉树的表现并不理想,因此需要一种更好的数据结构(在数据库中叫做索引)来加快查找,这就是B树或者B+树。
B-树(balance-search-tree)
一颗m阶的B-树或为空树,或为满足下列特性的m叉树:
(1)树中每个节点至多有m颗子树;
(2)若根节点不是叶节点,至少有两颗子树;
(3)除根之外的所有非叶子节点至少有m/2(上取整)颗子树;
(4)对于一个非叶子节点,其结构包含叶子节点大小n、然后按孩子指针、关键字的顺序间隔排列,从左往右,关键字的大小递增;
(5)所有的叶节点在同一层,不带任何信息,作为查找失败的点。
定义有点琐碎,来看看一颗B-树:
如图为一个5阶的B-树。这里有点像多层的顺序索引查找。
查找算法:
在节点内部可以按顺序查找或者二分查找的算法进行查找,如果找到了关键字就说明查找成功。如果没有找到就顺着指针往下找,如果到了叶节点还没有找到就说明查找失败。比如要查找关键字40,首先在根节点顺序查找(二分查找也可以),发现30<40<60,因此往下找,来到含40和50的节点,这里就找到了关键字40;如果要查找关键字45,跟40一样,来到40和50的节点,发现没有找到,同时又是最后一层了,所以查找失败。
性能分析:
模仿平衡二叉树的思路,我们先来看看深度为l的m阶B-树含有的最少结点数。
显然第一层至少有一个节点;
第二层至少有两个节点;
除根节点的每个非叶子节点至少有m/2(上取整)个子树,
因此第三层至少有2*(m/2)个节点;
第l层至少有2*(m/2)^(l-2)个节点,而第l层为叶子节点,即查找失败的节点;
如果m阶B-树有N个节点,则查找不成功的节点数为N+1个;
因此有下面公式:
N+1>=2*(m/2)^(l-1)
两边取对数得:
l<=logm/2((N+1)/2)+1
即从根节点到关键字上涉及的节点数不会超过上述值。
B-树的插入和删除:
插入和删除是两个相反的动作。
B-树的插入和删除原则是:
如果在插入和删除之后不影响B-树的性质,直接插入和删除;
如果在插入和删除之后影响B-树的性质,将要做相应的分裂或者合并处理;
(1)向B-树插入新关键字,插入后节点的关键字数小于等于m-1(m是阶数,就树的分支个数),则直接插入;
下图就是例子,其中b-树的阶m=3:
(2)如果想B-树插入新关键字,使得节点的关键字总数超过了m,需要分裂。分裂的办法是将节点中间的关键字上升至父节点;如果父节点的个数超过了m,同样进行分裂。
在删除的过程中,如果删除非叶子节点,可以让其子树的叶子节点的最小关键字代替之,并删除叶子节点即可,因此这里只讨论删除关键字在叶子节点的情况;
删除可以分为三种情况:
(3)如果向B-树删除一个关键字,所在节点的关键字数目不小于m/2,则直接删除;
(4)如果删除的关键字所在节点的关键字数目等于m/2-1,且其右兄弟的关键字数目大于m/2-1,则右兄弟的最小孩子S上移至父节点,父节点中小于S的关键字下移至被删除的节点中;(左兄弟以此类推)
(5)如果删除的关键字的所在关键字数目等于m/2-1,且其右兄弟的关键字数目也为m/2-1,则将它所在节点的剩余关键字加上父节点的关键字合并到其兄弟节点中;
比如删除24,就将37上升到父节点,同时30下移到删除的关键字所在的节点上。
B+树
B+树与m阶B-树的差异在于:
(1)有n颗子树的节点中含有n个关键字;
(2)所有叶子节点包含了全部的关键字信息,以及指向这些关键字记录的指针,且叶子节点本身依靠关键字大小从小到大链接;
(3)所有非叶子节点仅包含子树的最大(小)关键字,作为索引。
将所有叶子节点链接在一起的好处在于能够快速实现范围查找,比如我要查找15到60之间的数据,只需要查找关键字15,然后向后遍历即可。
为了能够在叶子节点得到所有满足条件的数据集,就必须使叶子节点包含全部的关键字信息,而非叶子节点仅作为索引,加快关键字的查找。
B+树的查找、插入、和删除跟B-树有相同之处。这里不再细说。
为什么是B-树而不是红黑树
先考虑这样一个场景,我在一个100万大小的文件中查找某个特定的记录,由于文件太大,它是存放在磁盘中的,根据操作系统的知识,磁盘读取速度远慢于内存读取速度,因此读取磁盘的次数将会直接影响查找时间。
那么如果用红黑树作为索引,其节点必须存放在磁盘上,所以树的高度即为读取磁盘的次数,那么log2(100 0 000)约为20,即需要读取磁盘20次;
如果是B-树或者B+树会怎样呢?
如果阶数m=100,那么如果是一个完全二叉树的话,log100(100 00 00)=3,只需读取3次磁盘。
高下立判。