B-树
我们一般叫“B树”,B是Balance平衡的意思。平衡树。
m阶-平衡树 (一个节点有m个地址域,m-1个数据域)
应用场景:文件索引系统的实现。
AVL树:二阶平衡树。一个节点有2个地址域,1个数据域。(左右孩子的高度差是1,0,-1)
B-树,所有叶子节点都在同一层,是非常平衡的一棵树!
m阶B-树:
最多有m个地址域(即m个孩子),m-1个数据域。
除根节点和叶子节点外,其他节点的孩子至少有一半个数的孩子(比如说有5个孩子,5/2=3,至少有3个孩子)。
除根节点之外的节点的关键字的个数:比如说是5阶的话。
5/2-1=3-1=2<=n<=4,不能到达5,到达5的话节点就要分裂。
B-树的插入操作
插入的元素数据都是带有顺序排的!
我们插入C,N,G,A,刚开始,只有这1个节点,也称根节点
我们现在要插入H,位置如下图所示
满了,此时中间元素是G,要把这个G提到父节点,产生一个父节点
为什么说每个节点至少有一半的孩子?
因为满了就要分裂,把中间的元素提到父节点,然后把左右两边分别形成父节点的左孩子,右孩子,此时左右孩子就都有一半。
现在我们要插入的是E,K,Q
现在我们要插入M,如下图所示,现在已经满了,我们要进行分裂。M应该插在K和N的中间。
所以我们把M这个中间节点提升到父节点当中。
现在父节点有3个孩子,2个数据域
叶子节点是绝对在同一层的,是相当绝对平衡的。
现在我们要插入F节点
F比G小,插在ACEF这里。
再插入W,L,T
现在要插入Z
Z大于M,应该插在M的右孩子。
右孩子已经满了。分裂。中间元素是T,所以把T提到父节点当中。
N,Q成为T的左孩子,W,Z成为T的右孩子
一个节点的元素都是排序插入的,而且左孩子的值都小于父节点的值,父节点的值小于其右孩子的值。
现在我们要插入D
D比G小,插在最左孩子中。
满了。D是中间元素,提到父节点当中。
AC是D的左孩子,EF是D的右孩子,即G的左孩子
现在插入P,R,X,Y
现在我们要插入S
S大于M,小于T
所以插在M的右孩子中,
满了,中间元素Q提到父节点当中,但是父节点也满了,
DGMQT。
中间是M,把M提出去,当父节点。
分裂都是向上分裂的哦。
是非常平衡的。
B-树的删除操作
依次删除H,T,R,E
我们删除H,因为H无左右孩子,所以直接删除就可以了
删除后,要看这个节点是否满足B树的特征呢:
叶子节点的元素数量 必须是:2<=n<=4
因为是5阶B树,满5个元素就要分裂,左右孩子各2个元素
删除H后,还有K,L,所以满足B树的特征
2个是元素的最基本的该有的数量
我们删除T,这个T有左右孩子的。
所以我们要上移孩子节点中的相近元素(左孩子最右边或者右孩子最左边的元素,哪边元素多,提哪边的)到父节点中去。
我们要把W提到父节点中。
我们现在要删除R节点。
因为R没有孩子。我们直接删除。
但是这个孩子现在就只有1个元素。所以不满足B树的性质了。
我们要看相邻的左右兄弟是否丰满(丰满就是元素个数要大于2个)
意思是从兄弟借一个。
如果兄弟丰满,则从父亲节点借一个元素来。把父节点的W借过来。然后把兄弟的X放上去父节点里面。
这样做才是符合排序树的性质的!
就是要1半的数量
现在我们看下面:我们要删除E直接删除E。不满足性质了。然后看相邻兄弟借。但是其相邻兄弟都刚脱贫(就是都只有2个元素)
并不丰满。
谁也借不了。
则该节点与其相邻的一个兄弟节点进行合并成1个新节点,以此来满足B树条件。(跟左边,或者右边都可以)
父节点D也放下来和ACE合并了,因为有2个孩子,所以要让父节点变为1个元素了。
现在父节点不丰满了,兄弟是刚刚脱贫,也借不了,只能合并
连接父节点,合并成1个节点
B-树的磁盘I/O优势和搜索效率
Windows和linux的文件搜索,以及数据库的索引。
都用到了B+树结构。
我们先看看B-树。
系统中的数据搜索:
数据量非常大,所以这些数据都是存储在磁盘上。
为了增加系统的搜索速度,我们一般会给数据创建索引。
创建索引,就是给数据进行排序,然后进行数据搜索就更加快速。
如果想对磁盘上存储的数据进行快速搜索查找需要解决的问题:
更少的磁盘I/O,以及更快的搜索速度、算法。
我们在这里采用m阶平衡树。
红黑树不是平衡树。AVL树是平衡树。
假如说,我们有AVL树和300阶的B-树。
在我们操作系统中,管理内存都是按页面page(4K)分配
管理磁盘(电脑的C盘E盘F盘)是按块block(16K)分配
我们的文件在磁盘存储的,从磁盘读1个文件,读4个字节,实际上,操作系统从磁盘是按块来读取的。
内存的单位是字节,管理的页面单位是页面4096字节。
磁盘的最小单位是扇区,一个扇区大小为512字节,16K除以512就是扇区的个数。
假如现在有1千万个索引需要从磁盘中进行读取和搜索,分别采用AVL树和B-树,看看花费是怎么样。
两者都是平衡树。搜索的过程是一样的。但是:
AVL树的高度:logn(log以2为底的n)
B-树的高度:log300n(log以300为底的n)(一个节点存300个数据)
如果用AVL树构建平衡二叉树来存储1千万个索引,需要24层节点。一个节点就是存储着一个索引数据,最差情况就是一个节点对应一个磁盘I/O,需要24次磁盘I/O,才找到
如果是用B-树,一般是取300-500阶,节点的大小刚好存储一块磁盘的数据。
操作系统发起一次磁盘I/O,会把这次磁盘I/O的所有数据都记录在块。
300阶,放299个数据。 log以300为底的N,来计算
也就是3层。
1千万的索引数据,对于一张数据库表来说,是上限了,再大就要分表了,B-树就是3层,最多花费3次磁盘I/O
搜索算法的时间复杂度都是:logn(log以2为底的n)
(进行的就是二分查找)
因为B-树中在每个元素节点里面采用的也是二分搜索算法,在每一层节点都进行二分查找
B+树的理论
数据库在存索引数据的过程:
读磁盘,磁盘上读数据,是按照块读取的。
图中是3阶的,2个数据域,3个地址域,图中的数字代表索引项(例如学号作为索引)。
关键是数据都在磁盘上,花费更少的磁盘I/O才是最重要的。
我们看B-树的每个节点存储的不仅仅是关键字,就是索引项,还存储着红色的点,这个红色的点代表索引项对应的记录,就是一行的数据。
每一个节点存储着不同的索引项,离根节点越近,命中更快,搜索最快,但是B-树总共就是3层,搜索是都挺快的。
B+树是B-树的变形树
同样的关键字都在叶子节点又出现了1次。
B+树的非叶子节点存放的都是关键字。
如何找到索引项对应的数据,数据都是在叶子节点上放的。
而且红点都在叶子节点(图中未显示)。
非叶子节点是叶子节点的索引,非叶子节点只存索引项,不存数据地址,没有红点。
索引项和数据都出现在叶子节点中。搜索每一个数据所花费的磁盘I/O都是一样的。
叶子节点中,全部的数据都串在一条有序的链表当中,
当我们在进行区间查找的时候,我们不用遍历复杂的B+树,我们直接遍历叶子节点就可以了。
B+树非叶子节点只存索引项,所以存的就多,找的就快,花费的磁盘I/O就少,查询花费的时间都是一样的(而且树的层数低)!
因为B+树不存储数据地址
B*树的理论
B*树是基于B+树的变种
叶子节点在一个链表上串着。
非叶子节点只存储关键字。
所有的数据和索引项都是在叶子节点上存储着。
它是在非根和非叶节点增加了指向兄弟节点的指针。
分裂:
左孩子满了,如果兄弟节点有位置,就把元素通过移到兄弟节点那里,节点值和父节点值交换一下。
然后左孩子就有空闲位置了,就可以插入新元素了。
还是搜索树的性质。
B*树分配节点的概率比B+树低。
节点更少,磁盘I/O次数更少。
左孩子满了,如果兄弟也满了
此时,增加1个新节点,从两边节点各拷贝1/3的数据。
如果索引的底层采用B*树就更好了