该笔记取决于 天勤的数据结构笔记
本文主要讲解b树和b+树的概念以及基本的代码逻辑
在讲解这部分知识时候,先科普一下一些基本概念作为入门了解
二叉排序树以及二叉平衡树
二叉排序树 (Binary Sort Tree) 又称二叉查找树,它是一种对排序和查找都很有用的特殊二叉树
二叉排序树的定义
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
(1) 若它的左子树不空,则左子树上所有结点的值均小千它的根结点的值;
(2) 若它的右子树不空,则右子树上所有结点的值均大千它的根结点的值;
(3) 它的左、 右子树也分别为二叉排序树。
二叉排序树是递归定义的。
由定义可以得出二叉排序树的一个重要性质:中序遍历一棵二叉树时可以得到一个结点值递增的有序序列
二叉排序树的二叉链表存储表示 具体代码结构如下:
typedef struct
KeyType key;
InfoType otherinfo;
}ElemType;
typedef struct BSTNode
{
ElemType data;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
二叉排序树的查找
具体的算法步骤为:
BSTree SearchBST (BSTree- T, KeyType key)
{//在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素
//若查找成功 , 则返回指向该数据元素结点的指针, 否则返回空指针
if ((! T) || key==T->data. key) return T; //查找结束
else if (key<T->data. key) return SearchBST (T->lchild, key); //在左子树中继续查找
else return SearchBST (T->rchild, key) ; //在右子树中继续查找
}
二叉排序树上的查找和折半查找相差不大。但就维护表的有序性而言,二叉排序树更加有效,因为无需移动记录,只需修改指针即可完成对结点的插入和删除操作。因此,对于需要经常进行插入、 删除和查找运算的表,采用二叉排序树比较好。
二叉排序树的插入
二叉排序树的插入操作是以查找为基础的。要将一个关键字值为key的结点*S 插入到二叉排序树中,则需要从根结点向下查找,当树中不存在关键字等千key的结点时才进行插入。新插入的结点一定是一个新添加的叶子结点,并且是查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子结点。
算法步骤如下:
void InsertBST(BSTree &T,ElemType e}
{//当二叉排序树 T中不存在关键字等千e.key的数据元素时, 则插入该元素
if (!T}
{
//找到插入位置 , 递归结束
S=new BSTNode; //生成新结点*S
S->data=e; //新结点*S的数据域置为e
S->lchild=S->rchild=NULL; //新结点*S作为叶子结点
T=S; //把新结点*S链接到已找到的插入位置
}
else if (e. key<T->data. key)
InsertBST(T->lchild, e ); //将*S插入左子树
else if (e.key> T->da七a.key)
InsertBST(T->rchild, e); //将*S插入右子树
}
二叉排序树的创建
二叉排序树的创建是从空的二叉排序树开始的, 每输入一个结点, 经过查找操作, 将新结点插入到当前二叉排序树的合适位置。
算法步骤:
void CreatBST(BSTree &T)
{//依次读人一个关键字为key的结点, 将此结点插人二叉排序树T中
T=NULL; //将二叉排序树T初始化为空树
cin>>e;
while(e.key!=ENDFLAG)
{
InsertBST(T,e);
cin>>e;
}
从上面的插入过程还可以看到, 每次插入的新结点都是二叉排序树上新的叶子结点, 则在进行插入操作时, 不必移动其他结点, 仅需改动某个结点的指针, 由空变为非空即可。 这就相当千在一个有序序列上插入一个记录而不需要移动其他记录
二叉排序树的删除
被删除的结点可能是二叉排序树中的任何结点, 删除结点后, 要根据其位置不同修改其双亲结点及相关结点的指针, 以保持二叉排序树的特性
void DeleteBST(BSTree &T,KeyType key)
{//从二叉排序树 T 中删除关键字等千 key 的结点
p=T;f=NULL; //初始化
/*------------下面的 while 循环从根开始查找关键字等于 key 的结点*p---------------*/
while (p)
{
if(p->data.key==key) break; //找到关键字等于 key 的结点*p, 结束循环
f=p; //*f 为*p 的双亲结点
if(p->data.key>key) p=p->lchild; //在*p 的左子树中继续查找
else p=p->rchild; //在*p 的右子树中继续查找
}
if (!p) return; //找不到被删结点则返回
//----考虑3种情况实现p 所指子树内部的处理: *p 左右子树均不空、 无右子树、 无左子树
if ((p->lchild) && (p->rchild)) //被删结点*p 左右子树均不空
{
q=p; s=p->lchild;
while (s->rchild) //在*p 的左子树中继续查找其前驱结点,即最右下结点
{
q=s; s=s->rchild; //向右到尽头
}
p->data=s->data; //s 指向被删结点的 “前驱"
if(q!=p) q->rchild=s->lchild; //重接*q 的右子树
else q->lchild=s->lchild; //重接*q 的左子树
delete s;
return;
}
else if (! p->rchild)
{
q=p; p=p->lchild; //被删结点*p 无右子树, 只需重接其左子树
)
else if (!p- > lchild)
{
q=p; p=p->rchild; //被删结点*p 无左子树, 只需重接其右子树
}
//--将 p 所指的子树挂接到其双亲结点*f 相应的位置--*/
if(!f) T=p; //被删结点为根结点
else if(q==f->lchild) f->lchild=p;
else f->rchild=p;
delete q;
}
二叉排序树查找算法的性能取决于二叉树的结构,而 二叉排序树的形状则取决于其数据集。如果数据呈有序排列,则二叉排序树是线性的,查找的时间复杂度为O(n); 反之,如果二叉排序树的结构合理,则查找速度较快,查找的时间复杂度为 O(lo2n)。事实上,树的高度越小,查找速度越快。因此,希望二叉树的高度尽可能小。
平衡二叉树或者是空树,或者是具有如下特征的二叉排序树:
(1 )左子树和右子树的深度之差的绝对值不超过1;
(2)左子树和右子树也是平衡二叉树。
若将二叉树上结点的平衡因子(Balance F a ctor, BF)定义为该结点左子树和右子树的深度之差,则平衡二叉树上所有结点的平衡因子只可能觅-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1 则该二叉树就是不平衡的
平衡二叉树的调整过程
插入结点时, 首先按照二叉排序树处理, 若插入结点后破坏平衡二叉树的特性, 需对平衡二叉树进行调整。
调整方法是:找到离插入结点最近且平衡因子绝对值超过1的祖先结点, 以该结点为根的子树称为最小不平衡子树, 可将重新平衡的范围局限于这棵子树
假设表中关键字序列为(13, 24, 37, 90, 53)。
一般情况下,假设最小不平衡子树的根结点为 A, 则失去平衡后进行调整的规律可归纳为4种情况
(1) LL 型:由于在 A 左子树根结点的左子树上插入结点,A的平衡因子由 1 增至 2, 致使以A为根的子树失去平衡,则需进行一次向右的顺时针旋转操作
例子如下:
(2) RR 型:由于在 A 的右子树根结点的右子树上插入结点, A 的平衡因子由-1 变为-2,致使以 A 为根结点的子树失去平衡,则需进行一次向左的逆时针旋转操作
例子如下:
(3) LR型:由于在A的左子树根结点的右子树上插入结点, A的平衡因子由1增至2,致使以A为根结点的子树失去平衡, 则需进行两次旋转操作。 第一次对B及其右子树进行逆时针旋转, C转上去成为B的根, 这时变成了LL型, 所以第二次进行LL型的顺时针旋转即可恢复平衡。 如果C原来有左子树, 则调整C的左子树为B的右子树
例子如下:
(4) RL 型:由于在 A 的右子树根结点的左子树上插入结点, A 的平衡因子由-1 变为-2,,致使以 A 为根结点的子树失去平衡, 则旋转方法和 LR 型相对称, 也需进行两次旋转, 先顺时针右旋, 再逆时针左旋
例子如下:
一棵m阶的B树,或为空树,或为满足下列特性的m叉树:
所有的叶子结点都出现在同一层次上,并且不带信息
,通常称为失败结点(失败结点并不存在,指向这些结点的指针为空。引入失败结点是为了便于分析B-树的查找性能);所有的非终端结点最多有m- 1个关键字
,因为他是m叉数结点的结构如图
(1)所有叶子结点均在同一层次,这体现出其平衡的特点。
(2 ) 树中每个结点中的关键字都是有序的,且关键字Ki;"左子树” 中的关键字均小于Kj;, 而 其 “右子树” 中的关键字均大于Ki;, 这体现出其有序的特点。(平衡二叉树)
(3 )除叶子结点外,有的结点中有一个关键字,两棵子树,有的结点中有两个关键字,三棵子树,这种4阶的B-树最多有三个关键字,四棵子树,这体现出其多路的特点
== B数查找==
Result SearchBTree(BTree T,KeyType key)
{//在 m 阶 B-树 T 上查找关键字 key, 返回结果(pt,i, tag)
//若查找成功,则特征值 tag=l, 指针 pt 所指结点中第 J. 个关键字等千 key
//否则特征值 tag=O, 等千 key 的关键字应插入在指针 pt 所指结点中第 1 和第迁1 个关键字之间
p=T;q=NULL;found=FALSE;i=O; //初始化, p 指向待查结点, q 指向 p 的双亲
while (p&& ! found)
{
i=Search(p,key);
//在 p-> key [ 1 .. keynum]中查找 i, 使得: p->key[i] <=keykey[i+l]
if(i>O&&p->key[i]==k) found=TRUE; //找到待查关键字
else{q=p; p=p->ptr[i];
}
if (found) return (p, i, 1); //查找成功
else return(q,i,0); //查找不成功,返回K的插人位置信息
}
插入
B树是动态查找树, 因此其生成过程是从空树起,在查找的过程中通过逐个插入关键字而得到
但由于B树中除根之外的所有终端结点中的关键字个数必须大于等千【 m/2 】-1 因此,每次插入一个关键字不是在树中添加一个叶子结点,而是首先在最低层的某个终端结点中添加一个关键字,若该结点的关键字个数不超过m-1 则插入完成,否则表明结点已满,要产生结点的 “分裂",将此结点在 同一层分成两个结点。
一般情况下,结点分裂方法是:以中间关键字为界,把结点一分为二,成为两个结点,并把中间关键字向上插入到双亲结点上,若双亲结点巳满,则采用同样的方法继续分解。最坏的情况下,一直分解到树根结点,这时B树高度增加1。
演示一个3阶的b树的具体过程
插入30 从上往下找
插入 26之后,已经超出了3阶
所以会分裂中间的30上去
插入85之后,已经超出了3阶
更适合用于文件索引系统
一棵m阶的 B+ 树和m阶的 B-树的差异在于:
非叶子节点可以看成是索引部分
,结点中仅含有其子树(根结点)中的最大(或最小)关键字通常在B+树上有两个头指针,一个指向根结点另一个指向关键字最小的叶子结点。
因此,可以对 B+树进行两种查找运算: 一种是从最小关字起顺序查找,另一种是从根结点开始,进行随机查找
B+树不仅能够有效地查找单个关键字,而且更适合查找某个范围内的所有关键字
任何数据结构都是应用到实际场景中
为了加深印象
举例一个数据结构应用的例子
为什么数据库是B+树而不是B树呢
树高度越小,I/O次数越少。
再者MySQL的两种搜索引擎可看我之前的文章
Mysql的两种存储引擎详细分析及区别(全)
具体为啥innodb的io次数少,这是因为
Innodb中的主键索引和实际数据时绑定在一起的,也就是说Innodb的一个表一定要有主键索引,如果一个表没有手动建立主键索引,Innodb会查看有没有唯一索引,如果有则选用唯一索引作为主键索引,如果连唯一索引也没有,则会默认建立一个隐藏的主键索引(用户不可见)。(用户没有指定的话会自己找生产一个隐藏列Row_id来充当默认主键)
另外,Innodb的主键索引要比MyISAM的主键索引查询效率要高(少一次磁盘IO),并且比辅助索引也要高很多。
innodb通过辅助索引的索引值找到主键索引,之后主键索引有具体的data值以及索引信息,都存储在里