图解B树和B+树

B树

B 树是为了磁盘或其它存储设备而设计的一种多叉平衡查找树,多叉就是多个分支的意思,二叉树就是最多只有两个分支的树。如下图所示,即是一棵 B树。

    

一棵 m 阶的 B 树必须满足如下条件:

    1)每个结点最多含有 m 个分支,也就是说:每个节点最多 m−1 个关键字。

    2)根节点最少可以有 1 个关键字,其它节点最少有 ⌈m2⌉−1 个关键字。

    3)每个节点的内部结构为:n 为节点中关键字的个数,Ki,i=1,2,...,n 为关键字,从小到大排列,Pi,i=0,1,...,n为指向关键字满足

       [Ki,Ki+1]范围的孩子节点。

       

这里认为上面的 B 树高度为 3,第三层就是叶子节点。至于有些材料说那些 null 是叶子节点,上述B树为4层,简直扯淡。

这里,我们再结合数据库索引说一下B树的用途,例如在SQLite中,使用了B树和B+树,如果key只是一个int值(比如主键,而不是整条数据库中的一条记录),那么将int值作为key存到B树中,会比存到B+树中更高效,因为B树的中间节点已经附带了key,而这些key是不需要在叶子节点中再保存一份的,也就是叶子结点和内部节点没有key的交集。但是如果把record也都放到了节点中,则B树就不如B+树快了,因为SQLite为了加快搜索,一个节点一般都是物理磁盘页的整数倍,所以在相同的node占用空间大小的情况下,B树的一个node存放的item更少(因为record肯定比4字节大),造成树更高,而B+ 树的一个节点就会存放更多item,树更矮,查找效率更高。

B 树的节点类型定义如下,这个定义只是用来查找内存数据的(也就是逻辑结构,主要是为了讲解和程序中快速获取对应的字段,并不是磁盘上的存储结构),如果用来查找外存,代码需要调整一下,下面叙述。

1

2

3

4

5

6

7

8

typedef int KeyType;

struct BTNode {

 int keyNum;       // 关键字个数

 struct BTNode *parent;  // 指向父节点(双向的B树)

 struct BTNode **ptr;   // 子树指针向量(数组), ptr[0],ptr[1],...,ptr[keyNum]

 KeyType **key;       // 关键字向量, key[0],key[1],...,key[keyNum-1]

}

B 树设计的目的是用来查找磁盘的,为了简单,假设每个盘块正好存放一个 B树的结点,这里用少量数据构造一棵 3 叉树的形式,来描述文件查找的具体过程(下面这个B树是一个磁盘文件索引树,操作系统一般都提供了对某个盘的建立索引的功能)。

    

 

上面的图中比如根结点,其中 17 表示一个磁盘文件的文件名(是一个string类型,而不是int);小红方块表示这个 17 文件内容在硬盘中的存储位置;P1表示指向 17 左子树的指针。

此时节点类型定义如下:

1

2

3

4

5

6

7

8

9

typedef char* KeyType;

struct BTNode {

 int keyNum;              // 关键字个数

 struct BTNode *parent;   // 指向父节点

 struct BTNode **ptr;     // 子树指针向量, ptr[0],ptr[1],...,ptr[keyNum],每个元素存放另外一个盘块的地址

 KeyType **key;           // 关键字向量, 存储的是文件名

 FILE_HARD_ADDR *offset;  // 存储每个文件(关键字)的磁盘地址

}

下面来模拟下查找文件 29 的过程:

    1)根据根结点指针找到文件目录的根磁盘块 1,将其中的信息导入内存。【磁盘 IO 操作 1 次】 

    2)此时内存中有两个文件名 17、35 和三个存储其他磁盘页面地址的数据。根据算法我们发现:17<29<35,因此我们找到指针 P2。

    3)根据 P2 指针,我们定位到磁盘块 3,并将其中的信息导入内存。【磁盘 IO 操作 2 次】

    4)此时内存中有两个文件名 26、30 和三个存储其他磁盘页面地址的数据。根据算法我们发现:26<29<30,因此我们找到指针 P2。

    5)根据 P2 指针,我们定位到磁盘块 8,并将其中的信息导入内存。【磁盘 IO 操作 3 次】

    6)此时内存中有两个文件名 28、29。根据算法我们查找到文件名 29,并定位了该文件内存的磁盘地址。

分析上面的过程,发现需要 3 次磁盘 IO 操作和 3 次内存查找操作。关于内存中的文件名查找,由于是一个有序表结构,可以利用折半查找提高效率。

至于 IO操作是影响整个 B 树查找效率的决定因素。

根据上面的例子我们可以看出,对于辅存做 IO 读的次数取决于 B 树的高度。而 B 树的高度由什么决定的呢?

问题:一棵 m 阶 B树,关键字个数为 n,求高度 h(不包含叶子节点)的取值范围。

 

无论是兄弟够借还是兄弟不够借,都要让父节点的一个关键字下来,如果兄弟够借,则让兄弟还父节点一个关键字,否则就不还了, 并且将自身、兄弟节点与借下来的关键字合并。如果没有还父节点一个关键字,此时需要递归判断父节点是否违背了条件。若违反条件则继续让祖父节点中的关键字先来,然后又是兄弟够不够借的问题。

    b. 当被删除的关键字不在终端节点:用需要被删除关键字的前驱关键字或后继关键字替换后,会变成情况 1。

       找前驱:就是从关键字 k 所在节点的左子树一直往右找。找后继:从关键字 k 所在节点的右子树一直往左找。举个例子:

       如下图,要删除的项在非叶子节点上。比如删除关键字 M。

           

       找到对应的中序前驱,从关键字 M 左边的指针指向的左子树开始一直往右走,最终可以找到前驱为 L,将 L,M做个交换,此时 M 在叶子节点上,删除 M,如上右图。删除之后树不平衡,但发现兄弟节点可以借项给它,按照情况 1 进行调整即可,调整后如下图:

        

 

B+ 树:是应文件系统所需而产生的一种 B 树的变形树。一棵 m 阶的 B+ 树和 m阶的 B 树的异同点在于:

    1)每个节点分支数和 B 树一致。

    2)B+ 树节点内关键字的个数就等于该节点的分支数,而不是如 B 树那样减 1。

    3)B+ 树改进了 B 树, 让内结点只作索引使用, 去掉了其中指向 data record 的指针, 使得每个结点中能够存放更多的 key, 因此能有更大的出度。

       下面左图是 B 树,右图是 B+ 树。data 就是文件地址。

              

    4)所有父节点的元素都同时存在于子节点,是子节点中是最大(或最小)元素。举个例子:

            

       在上面这棵树中,根节点的元素 8 是节点 2,5,8 和 节点 6,8的最大元素,15 也是一样的。

   5)由于 4),所以 B+ 树的叶子节点会包含全部的关键字信息,并且每个叶子节点包含指向相应记录的指针,所有叶子节点形成一个链表。

为什么说 B+ 树比 B 树更适合实际应用中操作系统的文件索引和数据库索引?

   1)B+树的磁盘读写代价更低:B+ 树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对 B 树更小。如果把所有同一内 部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO 读写次数也就降低了。

   2)B+ 树的查询效率更加稳定:由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值