0
点赞
收藏
分享

微信扫一扫

设计模式相关问题

其生 2024-02-12 阅读 10

文章目录

那么在此之前,我们也已经学过很多的搜索结构了,我们来一起回顾一下:

1. 常见的搜索结构

2. 问题提出

如果数据量很大,比如有100G数据,无法一次放进内存中,那就只能放在磁盘上了,如果放在磁盘上,有时需要搜索某些数据,那么该如何处理呢?

但是呢,实际中我们去查找的这个key可能不都是整型:

那这时候可以做一个改动:

那这样做的问题是什么呢?

所以:

使用平衡二叉树搜索树的缺陷

那如果用哈希表呢?

使用哈希表的缺陷

那如何加速对数据的访问呢?

那我们今天要学的B-树其实就是多叉平衡搜索树

3. B-树的概念

一棵m阶(m>2)的B树(B树中所有结点的孩子个数的最大值称为B树的阶),是一棵M路的平衡搜索树,可以是空树或者满足一下性质的树:

大家可以对照上面的图先来自行理解一下B树的这些性质,等后面我们熟悉了B树的结构之后大家可以再来反复理解这几条性质为什么是这样。
在这里插入图片描述

4. B-树的插入分析

那下面我们就来学习一下B-树的插入是怎样的。

那为了方便讲解,也方便大家理解,我们这里选取B-树的阶数取小一点,给一个3:

但是呢,为了后续实现起来简单,节点的结构如下:

插入过程分析

那下面我们就来找一组数据分析一下插入的过程,用序列{53, 139, 75, 49, 145, 36, 101}构建B树的过程如下:

那我们再来插入几个看看:

再往下插入101:

插入过程总结

  1. 如果树为空,直接插入新节点中,该节点为树的根节点
  2. 树非空,找待插入关键字在树中的插入位置(注意:找到的插入节点位置一定在终端节点中)
  3. 检测是否找到插入位置(假设树中的key唯一,即该元素已经存在时则不插入)
  4. 按照插入排序的思想将该关键字插入到找到的结点中
  5. 检测该节点关键字数量是否满足B-树的性质:即该节点中的元素个数是否等于M,如果小于则满足,插入结束
  6. 如果插入后节点不满足B树的性质,需要对该节点进行分裂:
    申请新的兄弟节点
    找到该节点的中间位置
    将该节点中间位置右侧的元素以及其孩子搬移到新节点中
    将中间位置元素(新建结点成为其右孩子)提取至父亲结点中插入,从步骤4重复上述操作

5. B-树的代码实现

那下面我们就来写写代码

5.1 B-树的结点设计

那首先我们来定义一下B-树的结点:

5.2 B-树的查找

那我们先来实现一个find,因为后面插入也要先find嘛:

画个图我们来分析一下:

find就写好了:

pair<Node*, int> Find(const K& key)
{
	Node* parent = nullptr;
	//从根结点开始找
	Node* cur = _root;

	while (cur)
	{
		// 在一个结点中查找
		size_t i = 0;
		while (i < cur->_n)
		{
			if (key < cur->_keys[i])
			{
				break;
			}
			else if (key > cur->_keys[i])
			{
				++i;
			}
			else
			{
				return make_pair(cur, i);
			}
		}

		// 往孩子结点去跳
		parent = cur;
		cur = cur->_subs[i];
	}

	//没找到,返回parent
	return make_pair(parent, -1);
}

5.3 B-树的插入实现

接下来我们来写一下插入:
在这里插入图片描述

首先如果是第一次插入的话我们需要做一个单独处理:

if (_root == nullptr)
{
	_root = new Node;
	_root->_keys[0] = key;
	_root->_n++;

	return true;
}

那再往下呢就是已经有结点的情况下插入:

如果不存在,那就去插入(find顺便带回了要插入的那个目标位置的结点)
那我们接收一下find的返回值,在这个结点里面插入即可

InsertKey

那插入的时候需要保证结点里面关键字的顺序,可以用插入排序的思想把新的关键字插进去(如果是分裂之后向父亲插入的话,它可能还有孩子),那我们这里再单独封装一个InsertKey的函数:

void InsertKey(Node* node, const K& key, Node* child)
{
	int end = node->_n - 1;
	while (end >= 0)
	{
		if (key < node->_keys[end])
		{
			// 挪动key和他的右孩子
			node->_keys[end + 1] = node->_keys[end];
			node->_subs[end + 2] = node->_subs[end + 1];
			--end;
		}
		else
		{
			break;
		}
	}

	node->_keys[end + 1] = key;
	node->_subs[end + 2] = child;
	if (child)
	{
		child->_parent = node;
	}

	node->_n++;
}

插入和分裂

然后我们就可以调用InsertKey接口去插入关键字,但是插入的话:

最终完整的Insert函数:

bool Insert(const K& key)
{
	if (_root == nullptr)
	{
		_root = new Node;
		_root->_keys[0] = key;
		_root->_n++;

		return true;
	}

	// key已经存在,不再插入
	pair<Node*, int> ret = Find(key);
	if (ret.second != -1)
	{
		return false;
	}

	// 如果不存在,find顺便带回了要插入的那个目标位置的结点

	//因为后面可能需要分裂继续往父结点插入,
	//所以这里我们接收find的返回值直接命名为parent
	Node* parent = ret.first;

	//参数中的key,const修饰不能修改,定义一个newkey
	K newKey = key;

	//分裂的兄弟也要作为孩子插入到父结点,所以再定义一个child
	//当然第一次插入关键字的时候孩子是空
	Node* child = nullptr;

	//有可能多次分裂往上一直插入,所以需要写成循环
	while (1)
	{
		InsertKey(parent, newKey, child);
		// 没有满,插入就结束
		if (parent->_n < M)
		{
			return true;
		}
		else// 满了就要分裂
		{
			size_t mid = M / 2;
			// 分裂一半[mid+1, M-1]给兄弟
			Node* brother = new Node;
			size_t j = 0;
			size_t i = mid + 1;
			for (; i <= M - 1; ++i)
			{
				// 拷贝key和key的左孩子给兄弟结点
				brother->_keys[j] = parent->_keys[i];
				brother->_subs[j] = parent->_subs[i];
				//如果孩子不为空,链接父亲指针
				if (parent->_subs[i])
				{
					parent->_subs[i]->_parent = brother;
				}
				++j;

				// 拷走的key清除重置一下方便调式观察
				parent->_keys[i] = K();
				parent->_subs[i] = nullptr;
			}

			// 还有最后一个右孩子也拷过去
			brother->_subs[j] = parent->_subs[i];
			if (parent->_subs[i])
			{
				parent->_subs[i]->_parent = brother;
			}
			parent->_subs[i] = nullptr;

			//重新设置它们的有效关键字数量
			brother->_n = j;
			parent->_n -= (brother->_n + 1);//+这个1是因为还提走了中位数

			K midKey = parent->_keys[mid];
			//清除重置提走的mid中位数
			parent->_keys[mid] = K();


			if (parent->_parent == nullptr)// 说明分裂是根节点
			{
				//那要创建新的根
				_root = new Node;
				_root->_keys[0] = midKey;
				//上面分裂的结点及分裂出的兄弟成为新的根的孩子
				_root->_subs[0] = parent;
				_root->_subs[1] = brother;
				_root->_n = 1;

				//将原结点和分裂的兄弟链接到新的根上
				parent->_parent = _root;
				brother->_parent = _root;

				break;
			}
			else// 分裂的不是根,转换成往parent->parent 去插入parent->[mid] 和 brother
			{
				newKey = midKey;
				child = brother;
				parent = parent->_parent;
			}
		}
	}

	return true;
}

测试

那下面我们就来构建一棵树来测试一下:

6. B-树的删除(思想)

那下面我们来讲一下删除的思想:

所以下面我们重点来讨论终端结点的删除

7. B-树的高度

问:含n个关键字的m阶B树,最小高度、最大高度是
多少?(注:大部分地方算B树的高度不包括叶子结点即查找失败结点)

最小高度

首先我们来分析一下最小高度:

最大高度

那最大高度呢:

当然也可以算出关键字的总个数来求解:

8. B-树的性能

9. B-树的简单验证(中序遍历)

那B-树呢也是搜索树,同样满足左子树<根<右子树,那我们可以对它进行一个验证,看中序遍历是否能得到一个有序序列。

那下面我们就来实现一下B-树的中序遍历:

那理清了思路,我来实现一下代码:

// 左 根  左 根  ...  右
void _InOrder(Node* root)
{
	if (root == nullptr)
		return;

	size_t i = 0;
	//依次访问当前结点中每个关键字的左子树和根
	for (; i < root->_n; i++)
	{
		_InOrder(root->_subs[i]);//先访问左子树
		cout << root->_keys[i] << " ";//再访问根
	}

	_InOrder(root->_subs[i]);//再访问最后一个关键字的右子树
}

void InOrder()
{
	_InOrder(_root);
}

那我们来验证一下呗,中序遍历一下我们上面插入之后的那个B-树:

那对于B-树的讲解我们就先到这里…
在这里插入图片描述

举报

相关推荐

0 条评论