BTree和B+Tree和Hash索引详解

二叉查找树

二叉树具有以下性质:左子树的键值小于根的键值,右子树的键值大于根的键值。

如下图所示就是一棵二叉查找树,

对该二叉树的节点进行查找发现深度为1的节点的查找次数为1,深度为2的查找次数为2,深度为n的节点的查找次数为n,因此其平均查找次数为 (1+2+2+3+3+3) / 6 = 2.3次

二叉查找树可以任意地构造,同样是2,3,5,6,7,8这六个数字,也可以按照下图的方式来构造:

但是这棵二叉树的查询效率就低了。因此若想二叉树的查询效率尽可能高,需要这棵二叉树是平衡的,从而引出新的定义——平衡二叉树,或称AVL树。

平衡二叉树(AVL Tree)

平衡二叉树(AVL树)在符合二叉查找树的条件下,还满足任何节点的两个子树的高度最大差为1。下面的两张图片,左边是AVL树,它的任何节点的两个子树的高度差<=1;右边的不是AVL树,其根节点的左子树高度为3,而右子树高度为1;

扫描二维码关注公众号,回复: 1560751 查看本文章

如果在AVL树中进行插入或删除节点,可能导致AVL树失去平衡,这种失去平衡的二叉树可以概括为四种姿态:LL(左左)、LR(左右)、RL(右左)、RR(右右)。它们的示意图如下:

这四种失去平衡的姿态都有各自的定义:

LL:LeftLeft,也称“左左”。插入或删除一个节点后,根节点的左孩子(Left Child)的左孩子(Left Child)还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。

RR:RightRight,也称“右右”。插入或删除一个节点后,根节点的右孩子(Right Child)的右孩子(Right Child)还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。

LR:LeftRight,也称“左右”。插入或删除一个节点后,根节点的左孩子(Left Child)的右孩子(Right Child)还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。

RL:RightLeft,也称“右左”。插入或删除一个节点后,根节点的右孩子(Right Child)的左孩子(Left Child)还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。

AVL树失去平衡之后,可以通过旋转使其恢复平衡。下面分别介绍四种失去平衡的情况下对应的旋转方法。

LL的旋转。LL失去平衡的情况下,可以通过一次旋转让AVL树恢复平衡。步骤如下:

1. 将根节点的左孩子作为新根节点。

2. 将新根节点的右孩子作为原根节点的左孩子。

3. 将原根节点作为新根节点的右孩子。

LL旋转示意图如下:

RR的旋转:RR失去平衡的情况下,旋转方法与LL旋转对称,步骤如下:

1. 将根节点的右孩子作为新根节点。

2. 将新根节点的左孩子作为原根节点的右孩子。

3. 将原根节点作为新根节点的左孩子。

RR旋转示意图如下:

LR的旋转:LR失去平衡的情况下,需要进行两次旋转,步骤如下:

1. 围绕根节点的左孩子进行RR旋转。

2. 围绕根节点进行LL旋转。

LR的旋转示意图如下:

RL的旋转:RL失去平衡的情况下也需要进行两次旋转,旋转方法与LR旋转对称,步骤如下:

1. 围绕根节点的右孩子进行LL旋转。

2. 围绕根节点进行RR旋转。

RL的旋转示意图如下:

BTree特性

InnoDB存储引擎中默认每个页的大小为16KB,InnoDB在把磁盘数据读入到磁盘时会以页为基本单位,B-Tree结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同。

一个 m 阶的B树满足以下条件:

1. 每个结点至多拥有m棵子树;

2. 根结点至少拥有两颗子树(存在子树的情况下);

3. 除了根结点以外,其余每个分支结点至少拥有 m/2 棵子树;

4. 所有的叶结点都在同一层上,到达任何一个叶结点最短路径的长度都是相同的;

5. 有 n 棵子树的分支结点则存在 n-1 个关键字,关键字按照递增次序进行排列;

6. 每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)

7. 关键字数量需要满足ceil(m/2)-1 <= n <= m-1;

8. ki(i=1,…n)为关键字,且关键字升序排序。

9. Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)

模拟查找关键字29的过程:

1. 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】

2. 比较关键字29在区间(17,35),找到磁盘块1的指针P2。

3. 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】

4. 比较关键字29在区间(26,30),找到磁盘块3的指针P2。

5. 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】

6. 在磁盘块8中的关键字列表中找到关键字29。

分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。

B树上大部分的操作(插入、删除、查询)所需要的磁盘存取次数和B树的高度是成正比的,并且B树是尽量多的在节点上存储信息,保证导数尽量少,在B树中可以检查多个子结点,由于在一棵树中检查任意一个结点都需要一次磁盘访问,所以B树避免了大量的磁盘访问,减少了磁盘I/O

BTree操作模拟网站

BTree操作过程

插入

新结点一般插在第h层,通过搜索找到对应的结点进行插入,那么根据即将插入的结点的数量又分为下面几种情况。

1. 如果该结点的关键字个数没有到达m-1个,那么直接插入即可;

2. 如果该结点的关键字个数已经到达了m-1个,那么根据B树的性质显然无法满足,需要将其进行分裂。分裂的规则是该结点分成两半,将中间的关键字进行提升,加入到父亲结点中,但是这又可能存在父亲结点也满员的情况,则不得不向上进行回溯,甚至是要对根结点进行分裂,那么整棵树都加了一层。

其过程如下:

删除

同样的,我们需要先通过搜索找到相应的值,存在则进行删除,需要考虑删除以后的情况,

1. 如果该结点拥有关键字数量仍然满足B树性质,则不做任何处理;

2. 如果该结点在删除关键字以后不满足B树的性质(关键字没有到达ceil(m/2)-1的数量),则需要向兄弟结点借关键字,这有分为兄弟结点的关键字数量是否足够的情况。

1) 如果兄弟结点的关键字足够借给该结点,则过程为将父亲结点的关键字下移,兄弟结点的关键字上移;

2) 如果兄弟结点的关键字在借出去以后也无法满足情况,即之前兄弟结点的关键字的数量为ceil(m/2)-1,借的一方的关键字数量为ceil(m/2)-2的情况,那么我们可以将该结点合并到兄弟结点中,合并之后的子结点数量少了一个,则需要将父亲结点的关键字下放,如果父亲结点不满足性质,则向上回溯;

    其余情况参照BST中的删除。

其过程如下:

B+Tree特性

B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。

从上一节中的B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度

以一个m阶树为例:

1. 根结点只有一个,分支数量范围为[2,m];

2. 分支结点,每个结点包含分支数范围为[ceil(m/2), m];

3. 所有非叶子节点的关键字数目等于它的分支数量,关键字顺序递增【此处有争议:或者等于分支数量-1】;

4. 所有叶子结点都在同一层,且关键字数目范围是[ceil(m/2),m]

5. 所有非叶子节点的关键字可以看成是索引部分,这些索引等于其子树(根结点)中的最大(或最小)关键字【此处有争议】。

    例如一个非叶子节点包含信息: (n,A0,K0, A1,K1,……,Kn,An),其中Ki为关键字,Ai为指向子树根结点的指针,n表示关键字个数。即Ai所指子树中的关键字均小于或等于Ki,而Ai+1所指的关键字均大于Ki(i=1,2,……,n)。

6. 所有叶子节点之间都有一个链指针,指向相邻的后一个叶子结点的指针信息,主要是为了加快检索多个相邻叶子结点的效率考虑。

通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。

可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:

InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为〖10〗^3)。也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录。

实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2~4层。mysql的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。

其操作和B树的操作是类似的,不过需要注意的是,在增加值的时候,如果存在满员的情况,将选择结点中的值作为新的索引,还有在删除值的时候,索引中的关键字并不会删除,也不会存在父亲结点的关键字下沉的情况,因为那只是索引。

B+Tree操作过程

插入

l  例1:

往下图的3阶B+树中插入关键字9

首先查找9应插入的叶节点(最左下角的那一个),插入发现没有破坏B+树的性质,完毕。插完如下图所示:

l  例2:

往下图的3阶B+树插入20

首先查找20应插入的叶节点(第二个叶子节点),插入,如下图

发现第二个叶子节点已经破坏了B+树的性质,则把之分解成[20 21], [37 44]两个,并把21往父节点移,如下图

发现父节点也破坏了B+树的性质,则把之再分解成[15 21], [44 59]两个,并把21往其父节点移,如下图

l  例3:

往下图的3阶B+树插入100

首先查找100应插入的叶节点(最后一个节点), 插入,如下图

修改其所有父辈节点的键值为100(只有插入比当前树的最大数大的数时要做此步),如下图

然后重复Eg.2的方法拆分节点,最后得

删除

l  例1:

删除下图3阶B+树的关键字91

首先找到91所在叶节点(最后一个节点),删除之,如下图

没有破坏B+树的性质,删除完毕

l  例2:

删除下图3阶B+树的关键字97

首先找到97所在叶节点(最后一个节点),删除之,然后修改该节点的父辈的键字为91(只有删除树中最大数时要做此步),如下图

l  例3:

删除下图3阶B+树的关键字51

首先找到51所在节点(第三个节点),删除之,如下图

破坏了B+树的性质,从该节点的兄弟节点(左边或右边)借节点44,并修改相应键值,判断没有破坏B+树,完毕,如下图

l  例4:

删除下图3阶B+树的关键字59

首先找到59所在叶节点(第三个节点),删除之,如下图

破坏B+树性质,尝试借节点,无效(因为左兄弟节点被借也会破坏B+树性质),合并第二第三叶节点并调整键值,如下图

l  例5:

删除下图3阶B+树的关键字63

首先找到63所在叶节点(第四个节点),删除之,如下图

合并第四五叶节点并调整键值,如下图

发现第二层的第二个节点不满足B+树性质,从第二层的第一个节点借59,并调整键值,如下图

猜你喜欢

转载自www.cnblogs.com/yifanSJ/p/9170393.html