一、引言
1970年,R.Bayer和E.mccreight提出了一种适用于外查找的树,它是一种平衡的多叉树,称为B树(或B-树、B_树)。我们都知道二叉查找树的查找的时间复杂度是O(log N),其查找效率已经足够高了,那为什么还有B树和B+树的出现呢?难道它两的时间复杂度比二叉查找树还小吗?
答案当然不是,B树和B+树的出现是因为另外一个问题,那就是磁盘IO;众所周知,IO操作的效率很低,那么,当在大量数据存储中,查询时我们不能一下子将所有数据加载到内存中,只能逐一加载磁盘页,每个磁盘页对应树的节点。造成大量磁盘IO操作(最坏情况下为树的高度)。平衡二叉树由于树深度过大而造成磁盘IO读写过于频繁,进而导致效率低下。
所以,我们为了减少磁盘IO的次数,就你必须降低树的深度,将“瘦高”的树变得“矮胖”。一个基本的想法就是:
(1)、每个节点存储多个元素
(2)、摒弃二叉树结构,采用多叉树
这样就引出来了一个新的查找树结构 ——多路查找树。 根据AVL给我们的启发,一颗平衡多路查找树(B~树)自然可以使得数据的查找效率保证在O(logN)这样的对数级别上。
【注意】:B-树,即为B树。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是一种树。而事实上是,B-tree就是指的B树。
二、基本概念
1、B树:B 树是为了磁盘或其它存储设备而设计的一种多叉(下面你会看到,相对于二叉,B树每个内结点有多个分支,即多叉)平衡查找树。与本blog之前介绍的红黑树很相似,但在降低磁盘I/0操作方面要更好一些。许多数据库系统都一般使用B树或者B树的各种变形结构。
2、对比红黑树: B树与红黑树最大的不同在于,B树的结点可以有许多子女,从几个到几千个。那为什么又说B树与红黑树很相似呢?因为与红黑树一样,一棵含n个结点的B树的高度也为O(lgn),但可能比一棵红黑树的高度小许多,应为它的分支因子比较大。所以,B树可以在O(logn)时间内,实现各种如插入(insert),删除(delete)等动态集合操作
三、性质
1、性质:B 树又叫平衡多路查找树。一棵m阶的B 树 (m叉树)的特性如下:(ceil代表向上取整)
(1)根结点至少有2颗子树(除非B树只包含一个结点);
(2)每个中间结点都包含k-1个元素(关键字)和k个子树,其中 ceil(m/2) ≤ k ≤ m;
(3)每一个叶子结点都包含k-1个元素(关键字),其中 ceil(m/2)-1 ≤ k-1 ≤ m-1;
(4)所有叶结点在同一层上,B树的叶结点可以看成一种外部结点,不包含任何信息;
(5)每个结点中的元素(关键字)从小到大排列,结点当中k-1个元素(关键字)正好是k个孩子包含的元素(关键字)的值域划分;
(6)每个结点的结构为:
其中,Ki(1≤i≤n)为关键字,且Ki 2、实例:下面我们通过一个实例进行讲解: 1)结点的分支数等于关键字数+1,最大的分支数就是B-树的阶数,因此m阶的B-树中结点最多有m个分支,所以可以看到,上面的一棵树是一个3-阶B-树。 2)因为上面是一棵3阶B-树,所以非根非叶结点至少要有ceil(3/2)=2个分支。根结点可以不满足这个条件,图中的根结点有两个分支。 3)如果根结点中没有关键字就没有分支,此时B-树是空树,如果根结点有关键字,则其分支数比大于或等于2,因为分支数等于关键字数+1. 4)上图中除根结点外,结点中的关键字个数至少为1,因为分支数至少为2,分支数比关键字数多1,还可以看出结点内关键字都是有序的,并且在同一层中,左边结点内所有关键字均小于右边结点内的关键字,例如,第二层上的两个结点,左边结点内的关键字为12,23,30,他们均小于右边结点内的关键字46。 B-树一个很重要的特征是,下层结点内的关键字取值总是落在由上层结点关键字所划分的区间内,具体落在哪个区间内可以由指向它的指针看出。例如,第二层左边数第二个的结点内的关键字划分了三个区间,小于23,23到30,大于30,可以看出其下层中最左边结点内的关键字都小于23,中间结点的关键字在23和30之间,右边结点的关键字大于30. 5)上图中叶子结点都在第四层上,代表查找不成功的位置。 四、B树的操作 B树可视化的网站:[B-Trees]:(https://www.cs.usfca.edu/~galles/visualization/BTree.html),假定对高度为h的m阶B树进行操作。 1、查询: 以上图为例:若查询的数值为5: 第一次磁盘IO:在内存中定位(与17、35比较),比17小,左子树; 第二次磁盘IO:在内存中定位(与8、12比较),比8小,左子树; 第三次磁盘IO:在内存中定位(与3、5比较),找到5,终止。 整个过程中,我们可以看出:比较的次数并不比二叉查找树少,尤其适当某一节点中的数据很多时,但是磁盘IO的次数却是大大减少。比较是在内存中进行的,相比于磁盘IO的速度,比较的耗时几乎可以忽略。所以当树的高度足够低的话,就可以极大的提高效率。相比之下,节点中的元素多点也没关系,仅仅是多了几次内存交互而已,只要不超过磁盘页的大小即可。 2、插入: 对高度为k的m阶B树,新结点一般是插在叶子层。通过检索可以确定关键码应插入的结点位置。然后分两种情况讨论: 1、 若该结点中关键码个数小于m-1,则直接插入即可。 2、 若该结点中关键码个数等于m-1,则将引起结点的分裂。以中间关键码为界将结点一分为二,产生一个新结点,并把中间关键码插入到父结点(k-1层)中 重复上述工作,最坏情况一直分裂到根结点,建立一个新的根结点,整个B树增加一层。 例1:在下面的B树中插入key:4 第一步:检索key插入的节点位置如上图所示,在3,5之间; 第二步:判断节点中的关键码个数: 节点3,5已经是两元素节点,无法再增加。父亲节点 2, 6 也是两元素节点,也无法再增加。根节点9是单元素节点,可以升级为两元素节点。; 第三步:结点分裂: 拆分节点3,5与节点2,6,让根节点9升级为两元素节点4,9。节点6独立为根节点的第二个孩子。 最终结果如下图:虽然插入比较麻烦,但是这也能确保B树是一个自平衡的树。 例2:我们以关键字序列{1,2,6,7,11,4,8,13,10,5,17,9,16,20,3,12,14,18,19,15}创建一棵5阶B-树,我们将详细体会B-树的插入过程。 (1)确定结点中关键字个数范围 由于题目要求建立5阶B-树,因此关键字的个数范围为2~4 (2)根结点最多可以容纳4个关键字,依次插入关键字1、2、6、7后的B-树如下图所示: (3)当插入关键字11的时候,发现此时结点中关键字的个数变为5,超出范围,需要拆分,去关键字数组中的中间位置,也就是k[3]=6,作为一个独立的结点,即新的根结点,将关键字6左、右关键字分别做成两个结点,作为新根结点的两个分支,此时树如下图所示: (4)新关键字总是插在叶子结点上,插入关键字4、8、13之后树为: (5)关键字10需要插入在关键字8和11之间,此时又会出现关键字个数超出范围的情况,因此需要拆分。拆分时需要将关键字10纳入根结点中,并将10左右的关键字做成两个新的结点连在根结点上。插入关键字10并经过拆分操作后的B-树如下图: (6)插入关键字5、17、9、16之后的B-树如图所示: (7)关键字20插入在关键字17以后,此时会造成结点关键字个数超出范围,需要拆分,方法同上,树为: (8)按照上述步骤依次插入关键字3、12、14、18、19之后B-树如下图所示: (9)插入最后一个关键字15,15应该插入在14之后,此时会出现关键字个数超出范围的情况,则需要进行拆分,将13并入根结点,13并入根结点之后,又使得根结点的关键字个数超出范围,需要再次进行拆分,将10作为新的根结点,并将10左、右关键字做成两个新结点连接到新根结点的指针上,这种插入一个关键字之后出现多次拆分的情况称为连锁反应,最终形成的B-树如下图所示: 3、删除: 对于B-树关键字的删除,需要找到待删除的关键字,在结点中删除关键字的过程也有可能破坏B-树的特性,如旧关键字的删除可能使得结点中关键字的个数少于规定个数,这是可能需要向其兄弟结点借关键字或者和其孩子结点进行关键字的交换,也可能需要进行结点的合并,其中,和当前结点的孩子进行关键字交换的操作可以保证删除操作总是发生在终端结点上。 例1:下面举一个简单的例子:删除元素11. 第一步:判断该元素是否在叶子结点上。 该元素在叶子节点上,可以直接删去,但是删除之后,中间节点12只有一个孩子,不符合B树的定义:每个中间节点都包含k个孩子,(其中 ceil(m/2) <= k <= m)所以需要调整; 第二步:判断其左右兄弟结点中有“多余”的关键字,即:原关键字个数n>=ceil(m/2) - 1; 显然结点11的右兄弟节点中有多余的关键字。那么可将右兄弟结点中最小关键字上移至双亲结点。而将双亲结点中小于该上移关键字的关键字下移至被删关键字所在结点中即可。 例2:我们用刚刚生成的B-树作为例子,一次删除8、16、15、4这4个关键字。 (1)删除关键字8、16。关键字8在终端结点上,并且删除后其所在结点中关键字的个数不会少于2,因此可以直接删除。关键字16不在终端结点上,但是可以用17来覆盖16,然后将原来的17删除掉,这就是上面提到的和孩子结点进行关键字交换的操作。这里不能用15和16进行关键字交换,因为这样会导致15所在结点中关键字的个数小于2。因此,删除8和16之后B-树如下图所示: (2)删除关键字15,15虽然也在终端结点上,但是不能直接删除,因为删除后当前结点中关键字的个数小于2。这是需要向其兄弟结点借关键字,显然应该向其右兄弟来借关键字,因为左兄弟的关键字个数已经是下限2.借关键字不能直接将18移到15所在的结点上,因为这样会使得15所在的结点上出现比17大的关键字,所以正确的借法应该是先用17覆盖15,在用18覆盖原来的17,最后删除原来的18,删除关键字15后的B-树如下图所示: (3)删除关键字4,4在终端结点上,但是此时4所在的结点的关键字个数已经到下限,需要借关键字,不过可以看到其左右兄弟结点已经没有多余的关键字可借。所以就需要进行关键字的合并。可以先将关键字4删除,然后将关键字5、6、7、9进行合并作为一个结点链接在关键字3右边的指针上,也可以将关键字1、2、3、5合并作为一个结点链接在关键字6左边的指针上,如下图所示: 显然上述两种情况下都不满足B-树的规定,即出现了非根的双分支结点,需要继续进行合并,合并后的B-树如下图所示: 有时候删除的结点不在终端结点上,我们首先需要将其转化到终端结点上,然后再按上面的各种情况进行删除。在讲述这种情况下的删除方法之前,要引入一个相邻关键字的概念,对于不在终端结点的关键字a,它的相邻关键字为其左子树中值最大的关键字或者其右子树中值最小的关键字。找a的相邻关键字的方法为:沿着a的左指针来到其子树根结点,然后沿着根结点中最右端的关键字的右指针往下走,用同样的方法一直走到叶结点上,叶结点上的最右端的关键字即为a的相邻关键字(这里找的是a左边的相邻关键字,我们可以用同样的思路找到a右边的相邻关键字)。可以看到下图中a的相邻关键字是d和e,要删除关键字a,可以用d来取代a,然后按照上面的情况删除叶子结点上的d即可。 【注意】 ①、B树主要用于文件系统以及部分数据库索引,例如: MongoDB。而大部分关系数据库则使用B+树做索引,例如:mysql数据库; ②、从查找效率考虑一般要求B树的阶数m >= 3; ③、B-树上算法的执行时间主要由读、写磁盘的次数来决定,故一次I/O操作应读写尽可能多的信息。因此B-树的结点规模一般以一个磁盘页为单位。一个结点包含的关键字及其孩子个数取决于磁盘页的大小。 五、B-树的应用 为了将大型数据库文件存储在硬盘上,以减少访问硬盘次数为目的,在此提出了一种平衡多路查找树——B-树结构。由其性能分析可知它的检索效率是相当高的 为了提高 B-树性能’还有很多种B-树的变型,力图对B-树进行改进,比如B+树。