文章目录
- 1. 常见的搜索结构
- 2. 问题提出
- 3. B-树的概念
- 4. B-树的插入分析
- 5. B-树的代码实现
- 6. B-树的删除(思想)
- 7. B-树的高度
- 8. B-树的性能
- 9. B-树的简单验证(中序遍历)
那么在此之前,我们也已经学过很多的搜索结构了,我们来一起回顾一下:
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:
插入过程总结
- 如果树为空,直接插入新节点中,该节点为树的根节点
- 树非空,找待插入关键字在树中的插入位置(注意:找到的插入节点位置一定在终端节点中)
- 检测是否找到插入位置(假设树中的key唯一,即该元素已经存在时则不插入)
- 按照插入排序的思想将该关键字插入到找到的结点中
- 检测该节点关键字数量是否满足B-树的性质:即该节点中的元素个数是否等于M,如果小于则满足,插入结束
- 如果插入后节点不满足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-树的讲解我们就先到这里…