B树
小结:
问题的提出:树深度过大而造成的磁盘IO读写过于频繁,查找效率低
特点 | 场景 | |
---|---|---|
B树 | 1. 每个节点最多m个孩子; 2. 中间节点个数范围:[ ceil(m/2)-1, m - 1] 3. 所有叶子节点都在同一层; 4. 中间节点按升序排列 | |
B+树 | 1. 叶子节点包含全部关键字信息,叶子节点顺序链接; 2. 所有中间节点是索引部分 | 文件索引、数据库索引 |
B*树 | 1. 中间节点增加指向兄弟指针; 2. 至少2/3M个关键字(饱满的B+树) | 空间利用率高,分配新节点的概率比B+树低 |
B树(m阶B树):
- 定义(5点)
- 树的高度
- 插入、删除:O(logN)
插入:满了分裂
删除:1. 先删除、移动(子节点填充父节点)2. 调整(向兄弟借 或 合并)
B+树
- 区别(2点)
- B+树相比B树更适合文件索引或数据库索引?
B*树
- 定义(非根和非叶子节点增加指向兄弟的指针)
- B+树的分裂
(1). 分配新节点,1/2数据复制过去;(2). 父节点增加新节点指针 - B*树的分裂
(1)兄弟未满,将部分数据移给兄弟;更新原节点和父节点
(2)兄弟满了,增加新节点,各复制1/3;父节点增加指针
1. 前言
动态查找树:二叉查找树、平衡二叉树、红黑树、B-tree/B+tree/B*tree
前3者是典型的二叉查找树结构,查找时间复杂度为O(log2N)
实际问题:大规模数据存储中,由于树的深度过大而造成的磁盘I/O读写过于频繁,进而导致查找效率低下。因此我们该想办法降低树的深度,从而减少磁盘查找存取的次数。一个基本的想法就是:采用多叉树结构
因此提出一个新的查找树结构–平衡多路查找树,即B-tree
2. B-树
n个节点的B数高度O(logN),由于分支因子比较大,可能比红黑树的高度小很多。
插入、删除 O(logn)
B树:
上图:1. 内节点x内若含有n[x]个关键字,那么x将还有n[x]+1个子女;2. 叶子节点都处于相同的深度
B树的定义:
平衡多路查找树。一棵m阶的B树特性如下:(注:切勿简单的认为一棵m阶的B树是m叉树,虽然存在四叉树,八叉树,KD树,及vp/R树/R*树/R+树/X树/M树/线段树/希尔伯特R树/优先R树等空间划分树,但与B树完全不等同)
- 树中每个节点最多含有m个孩子(m >= 2)
- 除根节点和叶子节点外,其他每个节点至少有[ceil(m/2)]个孩子(取上限)
- 根节点至少有2个孩子(除非B树只有一个节点:根节点)
- 所有叶子节点都出现在同一层,叶子节点不包含任何关键字信息(可以看做是外部结点或查询失败的结点,指向这些结点的指针都为null);(注:叶子节点只是没有孩子和指向孩子的指针,这些节点也存在,也有元素。类似红黑树中,每一个NULL指针即当做叶子结点,只是没画出来而已)
- 每个非终端节点中包含有n个关键字信息: (n, p0, k1, p1, k2, p2, …, kn, pn).其中:
a) Ki(i = 1…n)为关键字,且关键字按顺序升序排序K(i-1) < Ki
b) Pi为指向子树根的节点,且指针P(i-1)指向子树中所有节点的关键字均小于Ki,但都大于K(i-1)
c) 关键字的个数n必须满足: [ceil(m/2)-1] <= n <= m-1.比如有j个孩子的非叶节点恰有j-1个关键码
B树的高度:
B树的类型和节点定义
struct BTNode{
int keyNum; // 实际关键字个数, keyNUM<M
BTNode* parent;
BTNode** ptr; // 子树指针常量:ptr[0],....ptr[keyNum]
KeyType *key; //关键字向量: key[0],...key[keyNum-1]
}
注意关键字数为KeyNum-1,子树指针数为KeyNum
上面的图中比如根结点,其中17表示一个磁盘文件的文件名;小红方块表示这个17文件内容在硬盘中的存储位置;p1表示指向17左子树的指针
文件查找的具体过程(涉及磁盘IO操作)
例如上图3叉树,我们来模拟查找文件29的过程:
- 根磁盘块1导入内存,找到指针p2【磁盘IO操作1次】
- 根据p2,定位到磁盘块3,导入内存,找到指针p2 【磁盘IO操作1次】
- 根据p2,定位到磁盘块8,导入内存,找到文件29 【磁盘IO操作1次】
共需要3次磁盘IO操作和3次内存查找操作,由于是有序列表,可以利用折半查找
B树的插入、删除操作
以一颗5阶B树实例进行讲解(树中任一节点至多含有4个关键字,5棵子树)
插入
针对一棵高度为h的m阶B树,插入一个元素时,首先在B树中是否存在,如果不存在,一般在叶子结点中插入该新的元素,此时分3种情况:
- 如果叶子节点空间足够,即该节点的关键字数小于m-1,直接插入叶子节点的左边或右边
- 如果满了,则将该节点进行分裂,将一半数量的关键字元素分裂到新的相邻右节点,中间关键字元素上移到父节点
- 如果中间关键字上移到父节点导致根节点满了,则继续进行分裂
删除
下面介绍删除操作,删除操作相对于插入操作要考虑的情况多点。
注意这个先后顺序,https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/03.02.md 举例说明了这个顺序对操作的影响
- 先删除,移动(删除节点,子节点上移填充父节点)
- 查找B树中需要删除的元素,如果存在,则删除,如果删除该元素后,首先判断该元素是否有左右孩子节点
- 如果有,则上移孩子节点中某相近元素到父节点(左孩子最大或右孩子最小)【先做这个操作】
- 如果没有,直接删除
- 调整 (向兄弟借 或 合并)
删除元素,移动相应元素之后,如果某结点中元素数目(即关键字数)小于ceil(m/2)-1,则需要看其某相邻兄弟结点是否丰满(结点中元素个数大于ceil(m/2)-1)
- 如果兄弟节点够,则向父节点借一个元素来满足条件,然后将最丰满的相邻兄弟结点中上移最后或最前一个元素到父节点中
- 如果其相邻兄弟都刚脱贫,即借了之后其结点数目小于ceil(m/2)-1,则该结点与其相邻的某一兄弟结点进行“合并”成一个结点,以此来满足条件。
- 合并,首先将父节点中元素下移到其子节点中 进行合并
- 继续判断是否上层需合并
3. B+-tree
B+-tree:是应文件系统所需而产生的一种B-tree的变形树
一棵m阶B+树和m阶的B树的异同点在于:
- 有n棵子树的节点中含有n-1个关键字(与B树相同)
- 所有叶子节点中包含了全部关键字的信息,以及指向含有这些关键字记录的指针,而且叶子节点本身依关键字的大小自小而大地顺序链接 (B树的叶子节点没有包括全部需要查找的信息)
- 所有非终端节点可以看成是索引部分,节点中仅含关键字(而B树的非终节点也包含要查找的有效信息)
3.1 为什么说B+-tree比B树更适合实际中操作系统的文件索引和数据库索引?
- B+-tree的磁盘读写代价更低。因为其内部节点没有指向关键字具体信息的指针,内部节点相对B树更小,容纳的关键字数量更多,一次性读入内存中的需要查找的关键字越多。I/O读写次数降低
- B+-tree查询效率更稳定。由于叶子节点才是最终文件内容,因此,所有关键字查询的路径长度相同,导致每个数据的查询效率相当。
- B+-tree只要遍历叶子节点就可以实现树的遍历,支持基于范围的查询。B树不支持范围查询
B*-tree
B*-tree是B+-tree的变体,在B+树的基础上,B*树中非根和非叶子节点再增加指向兄弟的指针;*B*树定义了非叶子节点关键字个数至少(2/3)M,即块的最低利用率为2/3(代替B+树的1/2),如下图所示:
B+树的分裂:当一个节点满时,(1). 分配一个新的节点,将原节点中的1/2的数据复制到新节点,(2). 最后在父节点中增加新节点的指针。
B+树的分裂只影响原节点和父节点,不会影响兄弟节点,所以它不需要指向兄弟的指针。
B*树的分裂:当一个节点满时,(1). 如果它的下一个兄弟节点未满,那么将一部分数据移动到兄弟节点中,(2). 再在原节点插入关键字,(3). 最后修改父节点中兄弟节点的关键字(因为兄弟节点的关键字范围改变了);(4). 如果兄弟也满了,则在原节点与兄弟节点之间增加新节点,并各复制1/3的数据到新节点,(5). 最后在父节点中增加新节点的指针。
所以,B*树的分配新节点的概率比B+树要低,空间使用率更高
总结
- B树:有序数组+平衡多叉树
- B+树:有序数组链表+平衡多叉树
- B*树:一棵丰满的B+树
无论是B树,还是B+树、b树,由 于根或者树的上面几层被反复查询,所以这几块可以存在内存中, 换言之,B树、B+树、B树的根结点和部分顶层数据在内存中,大部分下层数据在磁盘上