【数据结构】B树和B+树的笔记详细诠释

前言

该笔记取决于 天勤的数据结构笔记

本文主要讲解b树和b+树的概念以及基本的代码逻辑

在讲解这部分知识时候,先科普一下一些基本概念作为入门了解

二叉排序树以及二叉平衡树

1. 概念指引

1.1 二叉排序树

二叉排序树 (Binary Sort Tree) 又称二叉查找树,它是一种对排序和查找都很有用的特殊二叉树

二叉排序树的定义

二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
(1) 若它的左子树不空,则左子树上所有结点的值均小千它的根结点的值;
(2) 若它的右子树不空,则右子树上所有结点的值均大千它的根结点的值;
(3) 它的左、 右子树也分别为二叉排序树。

二叉排序树是递归定义的。
由定义可以得出二叉排序树的一个重要性质:中序遍历一棵二叉树时可以得到一个结点值递增的有序序列

二叉排序树的二叉链表存储表示 具体代码结构如下:

typedef struct 
KeyType key; 
InfoType otherinfo; 
}ElemType; 
typedef struct BSTNode 
{ 
ElemType data; 
struct BSTNode *lchild,*rchild; 
}BSTNode,*BSTree;

二叉排序树的查找
具体的算法步骤为:

  1. 若二叉排序树为空, 则查找失败,返回空指针。
  2. 若二叉排序树非空, 将给定值key与根结点的关键字T->data.key进行比较:
    • 若key等于T->data.key, 则查找成功,返回根结点地址;
    • 若key小于T->data.key, 则递归查找左子树;
    • 若key大于T->data.key, 则递归查找右子树。
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的结点时才进行插入。新插入的结点一定是一个新添加的叶子结点,并且是查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子结点。

算法步骤如下:

  1. 若二叉排序树为空,则待插入结点*S 作为根结点插入到空树中。
  2. 若二叉排序树非空,则将key与根结点的关键字T->data.key进行比较:
    • 若key小于T->data.key, 则将S 插入左子树;
    • 若key大千T->data.key, 则将
    S 插入右子树。
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插入右子树
}

二叉排序树的创建

二叉排序树的创建是从空的二叉排序树开始的, 每输入一个结点, 经过查找操作, 将新结点插入到当前二叉排序树的合适位置。

算法步骤:

  1. 将二叉排序树T初始化为空树。
  2. 读入一个关键字为key的结点。
  3. 如果读入的关键字key不是输入结束标志,则循环执行以下操作:
    • 将此结点插入二叉排序树T中;
    •读入一个关键字为 key 的结点。
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;
}

1.2 平衡二叉树

二叉排序树查找算法的性能取决于二叉树的结构,而 二叉排序树的形状则取决于其数据集。如果数据呈有序排列,则二叉排序树是线性的,查找的时间复杂度为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 型相对称, 也需进行两次旋转, 先顺时针右旋, 再逆时针左旋
在这里插入图片描述
例子如下:
在这里插入图片描述

2. B树

一棵m阶的B树,或为空树,或为满足下列特性的m叉树:

  • 树中每个结点至多有m棵子树;
    -若根结点不是叶子结点,则至少有两棵子树;
  • 除根之外的所有非终端结点至少有「m/2 棵子树;
  • 所有的叶子结点都出现在同一层次上,并且不带信息,通常称为失败结点(失败结点并不存在,指向这些结点的指针为空。引入失败结点是为了便于分析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] <=key<p->key[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阶

在这里插入图片描述
往上分裂一个中间的70,又超出了3阶
在这里插入图片描述

继续往上分裂
在这里插入图片描述

具体的代码过程如下:
在这里插入图片描述

3. B+树

更适合用于文件索引系统

一棵m阶的 B+ 树和m阶的 B-树的差异在于:

  1. 有n棵子树的结点中含有n个关键字;
  2. 所有的叶子结点中包含了全部关键字的信息,以及指向含这些关键字记录的指针,且子结点本身依关键字的大小自小而大顺序链接;
  3. 所有的非叶子节点可以看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字

通常在B+树上有两个头指针,一个指向根结点另一个指向关键字最小的叶子结点。

因此,可以对 B+树进行两种查找运算: 一种是从最小关字起顺序查找,另一种是从根结点开始,进行随机查找

在这里插入图片描述
具体的代码逻辑:

  • 查找:若非终端结点上的关键字等于给定值, 并不终止,而是继续向下直到叶子结点。因此,在B+树中,不管查找成功与否,每次查找都是走了一条从根到叶子结点的路径。B+树不仅能够有效地查找单个关键字,而且更适合查找某个范围内的所有关键字
  • 插入:仅在叶子结点上进行插入,当结点中的关键字个数大于m时要分裂成两个结点,它们所含关键字的个数分别介于 (m+1/2)的下线和 (m+1/2)的上线,它们的双亲结点中应同时包含这两个节点中的最大关键字
  • 删除: B+树的删除也仅在叶子结点进行,当叶子结点中最大关键字被删除时,其 在非端结点中的值可以作为一个 “分界关键字“ 存在。若因删除 而使结点中关键字的个数少于「m/2 时,其和兄弟结点的合 并过程亦和B-树类似

4. 总结

  • b数: 每个节点都存储key和data,叶子节点指针为null。
  • b+树:只有叶子节点存储data,叶子节点包含了这棵树的所有键值,叶子节点不存储指针。每个叶子节点增加一个指向相邻叶子节点的指针非叶子节点上是不存储数据的,仅存储键值

任何数据结构都是应用到实际场景中
为了加深印象
举例一个数据结构应用的例子

为什么数据库是B+树而不是B树呢

  • 因为它内节点不存储data,这样一个节点就可以存储更多的key
  • B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。那么B+树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而B树因为数据分散在各个节点,要实现这一点是很不容易的

树高度越小,I/O次数越少。

再者MySQL的两种搜索引擎可看我之前的文章
Mysql的两种存储引擎详细分析及区别(全)

具体为啥innodb的io次数少,这是因为

Innodb中的主键索引和实际数据时绑定在一起的,也就是说Innodb的一个表一定要有主键索引,如果一个表没有手动建立主键索引,Innodb会查看有没有唯一索引,如果有则选用唯一索引作为主键索引,如果连唯一索引也没有,则会默认建立一个隐藏的主键索引(用户不可见)。(用户没有指定的话会自己找生产一个隐藏列Row_id来充当默认主键)

另外,Innodb的主键索引要比MyISAM的主键索引查询效率要高(少一次磁盘IO),并且比辅助索引也要高很多。

innodb通过辅助索引的索引值找到主键索引,之后主键索引有具体的data值以及索引信息,都存储在里

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农研究僧

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值