数据结构——B树和B+树
- 一、B树
- 1.B树的特征
- 2.B树的插入操作
- 3.B树的删除操作
- 4.B树的缺点
- 二、B+树
- B+树的特征
平衡二叉树或红黑树的查找效率最高,时间复杂度是O(nlogn)。但不适合用来做数据库的索引树。
因为磁盘和内存读写速度有明显的差距,磁盘中存储的数据需要先读取到内存中才能进行高速的检索。而数据库当中存储着海量的数据,光是数据库索引就有可能占据几个GB甚至更大的空间。当我们要查找数据的时候,显然不可能把整个索引树读到内存中。因此,我们只能以索引树的节点为基本单元,每次把单一节点从磁盘读取到内存当中,进行后续操作。
如果磁盘当中的索引树是一棵平衡二叉树,查找的时候,在最坏情况下,磁盘I/O的次数等于索引树的高度。
为了减少磁盘I/O,我们需要把原本“瘦高”的树结构变得“矮胖”,让每一个节点承载更多的元素,拥有更多的孩子。
B树和B+树,就是这样的数据结构,因此它们非常适合做数据库和文件系统的索引。
一、B树
1.B树的特征
B树也被称为B-树(中间的横线是一个连接符,不是“B减树”!)
B树单一节点拥有的最多子节点数量,称为B树的“阶”。一个m阶的B树,具有如下几个特征:
- 根节点至少有两个子节点。
- 每个中间节点都包含k-1个元素(也被称为关键字)和k个孩子,其中m/2 <= k <= m。
- 每一个叶子节点都包含k-1个元素,其中m/2 <= k <= m。
- 所有的叶子节点都位于同一层。
- 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
如图是一个3阶的B树:
B树的查找过程(以上图中查找5为例):
B树查询中的比较次数其实不比平衡二叉树少,但是节点内部元素的比较是在内存中进行的,时间成本几乎可以忽略。真正影响性能的,是磁盘I/O的次数,只要树的高度降低,磁盘I/O次数就会相应减少,从而提高整体性能。
2.B树的插入操作
B树的插入操作,首先是在叶子节点进行的,可以分成两种情况:
1)插入元素未改变规则,即叶子节点元素数量不超过m-1
未改变规则,就不用做任何调整。
2)插入元素使叶子节点元素过多,超过m-1个元素
新插入的元素4使相应的叶子节点元素超过2个,打破了3阶B树的规则。于是,需要进行节点的“分裂”,把节点m/2位置的元素上升到父节点,剩余的左半边元素和右半边元素分成两个独立的叶子节点:
此时,父节点(2,4,6)的元素同样超过了2个,我们继续对父节点
进行分裂,元素4上升到祖父节点:
这一次并没有打破B树的规则,插入调整完毕。
3.B树的删除操作
分为4种情况:
1)删除元素在叶子节点,未改变规则
没有打破B树的规则,因此不用做任何调整。
2)删除元素在叶子节点,剩余元素不足,相邻兄弟节点有多余元素
如图所示,删除叶子节点的元素8之后,该节点少于1个元素。此时可以向左侧兄弟节点“借调”一个元素过来。但元素5不能直接放到元素8的位置,需要上升到父节点当中,再把元素5和8之间的元素6移动到元素8的位置:
3)删除元素在叶子节点,剩余元素不足,相邻兄弟节点没有多余元素
如上图所示,删除叶子节点的元素3之后,该节点少于1个元素,左右兄弟也没有多余的节点可以借调。此时我们可以把相邻叶子节点“合并”。删除的元素3和右侧相邻节点的元素8之间是元素6,我们可以把元素6和8合并成一个叶子节点:
4)删除元素在中间节点
如上图所示,删除的是中间节点的元素12,我们可以选择该元素的前驱或后继元素来顶替它的位置。在本例中,选择后继元素13更合适:
4.B树的缺点
B树不方便进行范围查询。
数据库的查询,不止涉及单一结果查询(比如查找学号是110235的学生),也会涉及一个区间内结果的查询(比如查找数学成绩60~80分的所有学生)。
对于前者,B树很容易实现,对于后者,由于区间内结果分布在各个层次的节
点当中,B树实现起来会非常烦琐,需要进行中序遍历,在父节点和子节点之间不断切换,给磁盘I/O带来很多负担。
二、B+树
B+树是B树的升级,它的结构设计为范围查询提供了便利。
B+树的特征
一个m阶的B+树具有如下几个特征:
- 有k个子树的中间节点包含k个元素(B树中是k-1个元素),每个元素不保存数据,所有数据都保存在叶子节点。
- 所有的叶子节点包含了全部元素,依照元素的大小升序排列,叶子节点之间用双向指针相连接。
- 所有中间节点的元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
如上图是一颗B+树,每一个父节点的元素都出现在子节点中,是子节点的最大(或最小)元素。父节点的所有元素都出现在子节点,因此叶子节点包含了整棵树的全量信息。并且每一个叶子节点都带有指向相邻叶子节点的指针,形成了一个双向有序链表。
B+树的设计,让我们可以很方便地进行范围查询。
要查找区间范围内的元素时,首先自顶向下,查找区间的最小值所在的叶子节点,再通过链表指针,遍历到相邻叶子节点,直到在叶子节点中找到区间最大值,这样就成功找出了区间内的所有元素。