【篇首语】:
渐渐地,面试时被问起数据结构,MySQL调优,索引的原理等。首先感谢《王道计算机考研》的一些课程,让我学到了很多。
其实在这个问题之前,应该先了解扇区、磁道、磁盘存储数据的方式等相关概念,于是为了减少IO交互有了索引,索引逐渐增多,于是就要在密集索引上面建立稀疏索引,B+树的叶子结点那一层的链表相当于是密集索引,非叶子结点层相当于是稀疏索引。
【AVL树】:
【前提】:
二叉排序树的性能主要由相对高度决定。
AVL树特殊在于——左右子树的高度之差绝对值不超过1 , 而且左右子树又是一棵平衡二叉树。
【平衡因子】:
定义结点左子树与结点右子树的高度差为该结点的平衡因子 , 则平衡二叉树结点的平衡因子的值只可能是 -1 、0 或 1 。
【 -1 】————这个结点的右子树比左子树高1。
【 0 】————这个结点的左子树和右子树一样高。
【 1 】————这个结点的左子树比右子树高1。
【图解平衡因子】:
【建立AVL树】:
平衡二叉树的建立过程和二叉排序树的建立过程是相似的 , 都是从一棵空树开始陆续插入结点。不同的地方在于对于平衡二叉树的建立过程中,由于插入结点可能会破坏结点的平衡性,所以需要进行平衡调整。
【最小不平衡子树】:
以距离插入结点最近的,平衡因子绝对值大于1的结点为根的子树称为最小不平衡子树。
后面AVL树的调整均是围绕《最小不平衡子树》这一概念进行的。
【平衡调整分类】:
每插入一个结点,都要检查二叉排序树的平衡性 , 如果出现不平衡,那么平衡调整首先要找出最小不平衡子树,然后对这个最小不平衡子树进行调整。
【LL调整】:
//导致这个平衡二叉树不平衡是因为——最小不平衡子树的根结点(距离插入点最近的且绝对值>1的结点)的左侧的左侧。(LL)
//上图中正好巧了——3结点即为最小不平衡子树的根结点。
【RR调整】:
【LR调整】:
【RL调整】:
【平衡调整具体操作】:
【一次的】:
像LL调整、RR调整是比较简单的 , 调整进行一次操作即可。
【复杂的】:
LR 、RL 就稍微复杂一些 , 需要两次调整才能调好。
// LR、RL 这种情况,最小不平衡子树根结点的平衡因子符号 和 其孩子的平衡因子符号是相反的。————这就没有口诀了。
【调整实战案例】:
依次把结点 ( 34 ,23 ,15 ,98 ,115 ,28 ,107 )插入到初始状态为空的平衡二叉排序树中 ,使得在每次插入后保持该树仍然是平衡二叉树。
【课件过程】:
【查找效率、AVL树结论】:
【2-3树】:
【前提】:
如果仍沿用二叉树,树的高度就会变高。——(树的高度越高意味着时间开销越大)。
所以我们需要一种新的数据结构——多路查找树。
23树是多路查找树的一种 , 23的意思就是包含两种节点(2、3是结点指针的数量)。
【定义】:
【1】:《2号结点》包含一个元素和两个孩子(或者没有孩子)。
(1)左子树包含结点的元素值小于该结点的元素值,右子树包含的结点的元素值大于该结点的元素值。(和二叉排序树是一样的)
(2)2结点要不有两个孩子,要不就没有孩子,不允许有一个孩子。
【2】:《3号结点》包含一大一小两个元素和三个孩子(或者没有孩子)。(两个元素按大小顺序排列好,小的在前,大的在后)
(1)左子树包含结点的元素值小于该结点较小的元素值,右子树包含的结点的元素值大于该结点较大的元素值,另外,中间子树包含的结点的元素值介于这两个元素值之间。
(2)3结点要不有三个孩子,要不就没有孩子,不允许有一个或两个孩子。
【3】:2-3树所有叶子结点都在同一层次。
【234树】:
【简述】:
包含了三种类型的结点;
《2号结点》和《3号结点》的定和之前的23树的定义是一样的。
【定义】:
【1】:《2号结点》包含一个元素和两个孩子(或者没有孩子)。
(1)左子树包含结点的元素值小于该结点的元素值,右子树包含的结点的元素值大于该结点的元素值。(和二叉排序树是一样的)
(2)2结点要不有两个孩子,要不就没有孩子,不允许有一个孩子。
【2】:《3号结点》包含一大一小两个元素和三个孩子(或者没有孩子)。(两个元素按大小顺序排列好,小的在前,大的在后)
(1)左子树包含结点的元素值小于该结点较小的元素值,右子树包含的结点的元素值大于该结点较大的元素值,另外,中间子树包含的结点的元素值介于这两个元素值之间。
(2)3结点要不有三个孩子,要不就没有孩子,不允许有一个或两个孩子。
【3】:《4号结点》包含小中大三个元素和四个孩子(或者没有孩子)。
(1)最左子树包含的结点的元素值小于该结点最小的元素值;第二个子树包含的结点的元素值大于最小的元素值且小于中间元素值;第三个子树包含的元素值大于中间元素值且小于最大元素值;最右子树包含的结点的元素值大于该结点最大的元素值。
(2)4结点要不有四个孩子,要不就没有孩子,不允许有一个或两个或三个孩子。
【4】:234树所有叶子结点都在同一层次。
【M-way Search Tree 缺点】:
因为在M-way Search Tree的定义中,没有对结点中关键字的个数进行限制,所以在构建的过程中无法保证它的平衡,如下图:
【B树】:
【简述】:
B树也是一种平衡的多路查找树,2-3树 和 2-3-4树都是B树的特例,我们把树中结点最大的孩子数目称为B树的阶。通常记作m。
一棵m阶B树或为空树,或为满足如下特性的m叉树:
【定义】:
【1】:
树中每个结点至多有m棵子树。(即至多含有m-1个关键字)(“两棵子树指针夹着一个关键字”)
【2】:
若根结点不是终端结点,则至少有两棵子树。(至少一个关键字)
【3】:
除根结点外的所有非叶结点至少有 [m/2] 棵子树。( 即至少含有 [m/2]-1 个关键字 )
// [m/2] 并向上取整是为了维护平衡的性质。
//这条性质是帮助我们去建立 / 判断 B树的结点是否需要分裂的关键性质~~~!!!
【4】:
所有非叶结点的结构如下:
【5】:
所有的叶子结点出现在同一层次上,不带信息。(就像是折半查找判断树中查找失败的结点)
【B树的查找操作】:
B树是多路查找树,二叉排序树是二路查找,B树是多路查找,所以它是二叉排序树的拓展。因此,B树的查找操作和二叉排序树的查找操作非常类似。
【查找过程】:
先让待查找关键字key和结点中的关键字比较,如果等于其中某个关键字,则查找成功。
如果和所有关键字都不相等,则看Key处在哪个范围内,然后去对应的指针所指向的子树中查找。
Eg : 如果Key比第一个关键字K1还小,则去P0指针所指向的子树中查找,如果比最后一个关键字Kn还大,则去Pn指针所指向的子树中查找。
【B树的插入操作】:
在二叉排序树中,仅需查找到需插入的终端结点的位置。但是,在B树中找到插入的位置后,并不能简单地将其添加到终端结点位置,因为此时可能会导致整棵树不再满足B树中定义中的要求。
【举例演示B树插入】:
【3阶B树数据】:
{ 20 , 30 , 50 , 52 , 60 , 68 , 70 }
【课件中的过程】:
由于m=3 , 所以除了根结点以外,非叶子结点至少有 [ 3 / 2 ] - 1 = 1 个关键字 , 最多有 3 - 1 = 2 个关键字。所以依次插入20和30两个关键字到结点。
接下来插入50,但是由于最多有2个关键字,所以这个结点不满足B树要求,需要分裂。
接下来插入52 , 由于50结点只有一个关键字,所以可以插入52。
接下来插入60,插入60之后该结点关键字数量又不符合要求,需要分裂。
【分裂过程】:
取中间关键字([3/2]=2)即52 , 由于根结点只含30一个关键字 , 可以将52和30合并到一起。接下来需要处理50和60这两个结点,由于30<50<52 , 60>52 , 所以50和60各自单独作为一个结点。
接下来插入68 , 由于60结点只有一个关键字,所以可以插入68。
接下来插入70 , 插入70之后该结点关键字数量又不符合要求,需要分裂。
//根结点不满足要求对根结点进行分裂。
类似的 , 取中间关键字 ( [3/2] = 2 )即52作为新的根结点的关键字。
【B树结点的分裂】:
取这个关键字数组中的中间关键字( [n/2] )作为新的结点 , 然后其他关键字形成两个结点作为新结点的左右孩子。
【总结结点分裂】:
结点的分裂 , 以及结点分裂所带来的连锁效果。
【B树的删除操作】:
【简述】:
B树中的删除操作与插入操作类似 , 但要稍微复杂一些 , 要使得删除后的结点中的关键字个数 >= [m/2]-1 , 因此将涉及结点的 “合并” 问题。由于删除的关键字位置不同 , 可以分为关键字在终端结点和不在终端结点上两种情况。
【终端结点】:
最底层的存储有效关键字的结点。(最底层的叶子结点的上面一层结点)
【详细定义】:
【1】:如果删除的关键字在终端结点上(最底层非叶子结点):
结点内关键字数量大于 [m/2]-1 , 这时删除这个关键字不会破坏B树的定义要求,所以直接删除。
结点内关键字数量等于 [m/2]-1 , 并且其左右兄弟结点中存在关键字数量大于 [m/2]-1 的结点 , 则去兄弟结点中借关键字。
结点内关键字数量等于 [m/2]-1 , 并且其左右兄弟结点中 《不》 存在关键字数量大于 [m/2]-1 的结点 , 则需要进行结点合并。
【2】:如果删除的关键字<不在>终端结点上(最底层非叶子结点):需要先转换成在终端结点上,再按照在终端结点上的情况来分别考虑对应的方法。
在说具体做法之前,首先需要介绍一个概念——【相邻关键字】。
【相邻关键字】:
对于不在终端结点上的关键字 , 它的相邻关键字是其左子树中值最大的关键字或者右子树中值最小的关键字。
存在关键字数量大于 [m/2]-1 结点的左子树 或者 右子树 , 在对应子树上找到该关键字的相邻关键字,然后将相邻关键字替换待删除的关键字。
左右子树的关键字数量均等于 [m/2]-1 , 则将这两个左右子树结点合并,然后删除待删除关键字。
【具体的删除例子】:
3阶B树
[3/2]-1 = 1. ——即:非根、非叶子 结点 的关键字数量至少是一个。
【删除9】:(在上图完整的树结构上独立删除)
因为9是终端结点,符合 1_(1) 的这种情况 , 所以直接删除即可。
【删除2】:(在上图完整的树结构上独立删除)
符合1_(2)的这种情况。
【删除16】:(在上图完整的树结构上独立删除)
–//意思就是不够借了 , 不够借了 ,这个时候就需要进行合并。
【合并方法】:
从上一层的结点取关键字 与 下一层的结点合并。
方式不唯一 ,可以从把关键字14取下来 和 11合并成一个结点 , 也可以把20和22合并成一个结点。
【删除10】:(在上图完整的树结构上独立删除)
符合2_(1)的情况。
【第1步】:
找出这个待删除关键字的相邻关键字 , 比如说下图中10的相邻关键字就是9或者是11, 其实就是这个大小序列中该关键字的直接前驱或者是直接后继关键字。
【第2步】:
将这个待删除的关键字和某个相邻关键字互换。
【删除14】:(在上图完整的树结构上独立删除)
【B+树】:
【简述】:
B+树是常用于数据库和操作系统的文件系统中的一种用于查找的数据结构。
【m阶的B+树 与 m阶的B树的主要差异】:
在B+树中 , 具有n个关键字的结点只含有n棵子树 , 即每个关键字对应一棵子树 ; 而在B树中 , 具有n个关键字的结点含有(n+1)棵子树。
在B+树中,每个结点(非根内部结点)关键字个数n的范围是 [m/2]<=n<=m (根结点1<=n<=m) , 在B树中 , 每个结点(非根内部结点)关键字个数n的范围是 [m/2]-1 <= n <= m-1 (根结点 : 1<=n<=m-1) 。
在B+树中 , 叶结点包含信息 , 所有非叶结点仅起到索引作用 , 非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。(而在B树中每个关键字对应一个记录的存储地址)
在B+树中,叶结点包含了全部关键字 , 即在非叶结点中出现的关键字也会出现在叶结点中,而且叶子结点的指针指向记录 ; 而在B树中,叶结点包含的关键字和其他结点包含的关键字是不重复的。
在B+树中 , 有一个指针指向关键字最小的叶子结点 , 所有叶子结点链接成一个单链表。
【实例】:
【B*树】:
【简述】:
Oracle数据库的索引所采用的数据结构,非叶子结点层也是链表的形式。
【新的变化】:
B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3代替B+树的1/2);
B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
所以B*树相对于B+树,空间利用率上有所提高,查询速率也有所提高。
【总结优点】:
在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率从1/2提高到2/3;