【技术点】数据结构--B树系列(四)

文章目录

  • 前言
  • B树/B-树
    • 树结构中的度
    • B树的结构
    • B树的搜索过程
    • 为什么要B树,或者说B树和二叉树的应用不同在哪里?
    • B树的插入
    • B树的删除
  • 下一步

前言

前几篇文章讲常用的二叉树结构都讲完了。
传送门:
【技术点】数据结构–二叉树(一)
【技术点】数据结构–二叉树(二)
【技术点】数据结构–二叉树之红黑树(三)
在树这种大的数据结构中,除了二叉树,还有几种树是在实际中应用的较多的,比如接下来的几篇文章想将的B树系列(B树/B-树/B+树/B*树)

B树/B-树

首先,这两个树就是同一个东西,只是在翻译的过程中,将“B-Tree”这个专业词汇中间的那根杠杠怎么理解的问题,忽略那根杠杠,就是B树;不忽略就是B-树。
下文统一用B树这个名称。

树结构中的度

在二叉树中,除叶子节点外,每个节点都有不超过2个子节点。那么 每个节点最多的节点数,我们称之为节点的度,或者阶,如果树结构中的节点的度为m,那么我们称这颗树为m阶树。
二叉树用于搜索的话,称作搜索二叉树。如果m阶树用于搜索,我们称之为多路查找树(多条搜索路径),更进一步的就是平衡多路查找树。

B树的结构

一颗M阶B树有如下特征:

  1. 节点保存在关键字,每个节点保存的关键字大于ceil(M/2) - 1,小于M-1。像下图的一颗3阶树,最少一颗,最大2个关键字。

  2. 节点的子节点数大于1,小于等于M。

  3. 每个叶子节点的高度相同。
    【技术点】数据结构--B树系列(四)_第1张图片
    我们可以用代码来定义一下这个结构:

     typedef BTreeNode {
         int keyNum;   //关键字的个数
         int[] keys; //保存关键字,顺序存储
         BTreeNode * sons[M];  //子节点指针, 最多为M个,若无则为NULL, 初始化都为NULL
         BTreeNode * parent;  //为了方便,增加一个父节点指针
     }
    

B树还具有以下特征(度娘上搬运):
【技术点】数据结构--B树系列(四)_第2张图片

B树的搜索过程

根据这个结构可知其搜索过程:

  1. 从根节点开始,将节点的关键字与搜索值(N)比较,如果找到了,则返回
  2. 设每个节点中的关键字个数为K(每个节点的k不相同,2
  3. 若 N的位置在关键字数组keys的第 k与k+1关键值之间,那么就去son数组对应的子指针的节点继续搜索
  4. 递归上述3个步骤

伪代码:

//伪代码中没有考虑各种为null的情况
function searchInBTree(T, x){
	for (i = 0; i < T.keys.num - 1; i++){  //最后一个关键字单独处理
		if T.keys[i] = x{    //找到了
			return T;
		}else if(x < T.keys[i]){
			searchInBTree(T.sons[i], x);    为什么是i,这个对应关系大家可以对着图比划一下就理解了
		}
	}
	//处理最后一个关键字
	if (T.keys[num] == x){
		return T
	} esle if (T.keys[num] > x){
		searchInBTree(T.sons[M], x);
	}
}

看到这里,我们需要把应该在最前面来写的问题摆出来了,也就是:

为什么要B树,或者说B树和二叉树的应用不同在哪里?

前面讲到二叉树的搜索分叉判断很简单,不是左子树就是右子树。而B树需要在节点的key值中找到相应的指针去递归搜索。增加了搜索操作的复杂度。
但是我们考虑一下实际场景,数据是保存在硬盘上的。假设一颗二叉树的结构保存在硬盘上,没索引一个二叉树节点,都需要进行一次I/O操作,如果深度比较大的话(数据库大表的索引本身也不是一个小数目),大量的I/O操作会占用大量的时间。那么这个时候B树就可以派上用场了。把多个key值放在同一个节点中可以缩短树的高度,减少大量的I/O。虽然我们需要对每个节点的key值进行搜索寻找下一步递归的指针,但是这个操作是在内存中进行的,而且搜索的长度也不会很大。从时间上来说,是可以减少搜索时间的。
因此,我们可以得到一个大概的原则:

  • 数据都保存在内存里,二叉树更方便
  • 数据需要进行I/O查询,B树更省时间

所以,很多文件系统的索引都是使用B树来实现,而不是二叉树。

B树的插入

B树的插入总体和二叉树的插入差不多,通过递归找到节点的位置再插入,这里需要多的一个步骤是,需要确定具体子树的指针。但是和二叉树不一样的也有几点:

  • 插入后会导致节点超过树的度从而造成节点的分裂
  • 在叶子节点上来插入数据,保证所有节点在同一层侧,如果需要分裂,把某个key值提升到parent节点去

我们拿一个随机的数列来构造一颗3度的B树:[ 3,4,1,5,6,9,0,7,8,2 ]
【技术点】数据结构--B树系列(四)_第3张图片
【技术点】数据结构--B树系列(四)_第4张图片
途中竖着的两颗树就表示节点发生分裂的过程:
我们可以把这个过程写成伪代码:

function addNodetoBTree(T, x){
	int degree = 3;  //3度树
	if (T is null){
		T = newNode();
		T.parent = null;
		T.keyNum = 1
		T.keys.add(x)
	}esle{
		if (T.keyNum < degree -1 ){
			T.keyNum ++;
			T.keys.add(x)  //顺序添加,这里不细写
		}esle(T.keyNum = degree){   //先增加、再分裂
			T.keyNum ++;
			T.keys.add(x)  //顺序添加,这里不细写
			if (T is root) {
				parent = newNode()
			}else{
				parent = T.parent
			}
			parent.keyNum++;
			parent.kes.add(T.keys[1])   //中间节点,5度树就是keys[2],如果是偶数就要规定一下
			T.keyNum--;
			T.keys.remove(T.keys[1]);
			for (i=0; i

B树的删除

和插入相反的是,B树的删除可能导致节点的合并。
在B树的删除操作里,和红黑二叉树类似地,需要引入后继与前继的概念。与二叉树不同的地方在于,而二叉树的前继与后继是节点,而B树里面的前继与后继key值。
前继和后继的key值都是在B树的叶节点里。
我们找到前继或者后继节点之后,可以和二叉树操作类似,把前继或者后继key添加到需要删除的节点里。这样我们就把问题转换成了从叶子节点里删除key值的问题。
这个问题我们分成两种情况:

  1. 叶子节点删除这个后继或者前继key值后,节点里的数字仍然超过规定(ceil(M/2) - 1),那么直接删除即可,不会破坏B树的结构
  2. 叶子节点删除这个后继或者前继key值后,,节点里的数字低于规定(ceil(M/2) - 1),那么需要对节点进行合并

合并的话,也就是需要从其他地方借用一个key值过来,使得自己满足规则。能从哪里借呢?我们可以从两个地方来借:

  • 兄弟节点
  • 父节点

和现实社会很相近对不对?有困难找亲戚。
还是老样子,我们用图来说明,继续上面的例子:
首先删除节点3:
【技术点】数据结构--B树系列(四)_第5张图片
步骤说明:

  1. 前继key值为1,把1插入到删除键值所在的节点
  2. 删除键值
  3. 把前继key值从所在节点删除,此时,所在节点剩下的key值 >= ceil(3) - 1,所以不需要做其他的操作。

继续,我们再删除节点1:
按照上述的步骤:
【技术点】数据结构--B树系列(四)_第6张图片
此时,我们需要进行合并了。此时,设删除key值为dkey,所在节点为dnode。
4. 从dnode的兄弟节点一起合并,此时dnode已经为空节点,合并了4节点之后也无法符合条件。
5. 兄弟不够,只有啃老,将父节点一起合并。
6. 合并完之后,B树结构处于不平衡的状态,继续进行合并,此时是将较长的一方进行合并(为了缩短路径)
见图:
【技术点】数据结构--B树系列(四)_第7张图片
从图中可以看出,在合并了0,4两个键值之后,造成了key=5所在节点的不平衡,从5往下进行合并来缩短路径(这是一个递归的过程,因为可能第一层无法进行合并)。
这就完了么?怎么可能,从前一句话,大家肯定可以看出来,就算一个递归的规程,假设长的一端递归到了叶子节点也无法合并呢?
考虑下这样一颗B树,根据前面的逻辑删除掉key=1之后是这么一个状况:
【技术点】数据结构--B树系列(四)_第8张图片
已经无法通过压缩根节点的除左子树意外的路径达到平衡了,此时,我们就需要对树进行旋转替换。
如图:
【技术点】数据结构--B树系列(四)_第9张图片

  1. 可以找到失衡子树(图中为最左子树)临近的后继或者前继key值。
  2. 将这个key值替换成失衡子树的父节点的相应key值
  3. 上一步的被替换的key值往失衡子树节点下沉
  4. 分拆与旋转失衡子树,使树达到平衡。

好了,删除节点的步骤基本上就是这些,上述的例子都是最左边的子树,而且是3阶树的操作,其他位置的其他阶树的B树,大家可以去推演一下。

下一步

这篇文章本来打算把B/B+/B*树一起讲完的,算了。太多了写起来累,看起来也累。下一篇再将B+树吧。

你可能感兴趣的:(技术点,数据结构,B树)