标签:只读 超级 parent 引用 核心 次数 二分查找 概述 结束
动机: B树实现高速I/O
640K如何"满足"任何实际需求了-- 源自比尔·盖茨的一个笑话
前提知识
问题:
B树为什么更宽更矮
B树实际也是BST,但是其对节点的定义方式不一样, 其定义的节点为超级节点
超级节点的概念:
如果我们将2代节点合并,并把它们都踩扁了,就会拥有4路,3个关键码
同理:每d代合并,就会拥有m =2^d路,m-1个关键码
对于m阶B树来说
关键码:
有不超过m-1个关键码
, 即n <= m - 1个关键码—— 有不少于
$\lceil \frac{m}{2} \rceil - 1 <= n$ 个关键码
分枝数
有不超过m个分支
, 即n + 1 <= m个分支数—— 有不少于
$\lceil \frac{m}{2} \rceil <= n + 1$个分支
简单举例:
外部节点深度统一相等
important
{:.error} B树中,外部节点更加名副其实,意味着查找失败,因此需要计算外部节点一般来说,B树的词条极多,所以其存在外存之中。
B树查找的核心:只载入必需的节点,尽可能减少IO操作
{:.info}
其大概思想就是将根节点存放在内存之中,当需要某些数据时,进行B树的查找,将其从外存中读入到内存。鉴于之前
谈过的数据的局部性。我们将新的查找到的超级节点也放到内存之中。因此在同样查找的情况下,B树的树高更低,
所执行的IO次数自然就少。因此我们借助比较小的内存,就可以实现大规模数据的高效操作
实例:
...
不难发现,整个B树查找的过程其实就是在顺序查找,IO操作,顺序查找,IO操作不停重复的过程。
B树的失败查找,一定结束于外部节点处。
B树的算法时间主要消耗在俩个方面
一个B树有N个内部节点,就一定有N+1个外部节点,从物理意义上理解,N个内部节点代表N种成功的可能,
自然对于N+1种失败的可能。
对于插入来说,自然要合理使用search接口(插入必定插入在叶子节点,说实话,树的插入没有位置的选项,所以只能
插入,然后让树自己去选择位置)。search帮我们找到了叶子节点_hot,然后我们通过find接口找到
_hot节点中大于target的第一个关键码,然后在这里新插入
BTree<T>::insert(const T &e)
{
auto ret = search(e);
if(ret) return false; // 保证目标元素不存在
++_size;
// search查找失败后,_hot就是叶节点
// 我们要在_hot中插入e
auto retIter = find(_hot->key.begin(), _hot->key.end(), [=](T t){
if(e>=t) return true;
return false;});
_hot->key.insert(retIter, e);
插入关键码后,自然也要插入孩子指针,而实际上如下图所示,因为使叶子节点
我们并不需要去寻找位置,只要在child向量中push_back一个空指针即可
// 理论上的插入
//int posnum = retIter - _hot->key.begin();
//_hot->child.insert(_hot->child.begin() + posnum + 1, nullptr );
// 但是叶子节点后面全是外部节点,所以都是nullptr,不需要再特定位置插入
_hot->child.push_back(nullptr);
// 插入之后,可能会导致这个节点的关键码超过m-1,从而溢出,违反了B树的定义。
solveOverflow(_hot);
}
/*
superNode-_hot ┌───┐
│ e │
┌─────────┐ ┌───┬───┬───┬───┬─┴─┬─┴─┬───┬───┐
│ key-set │───? │ s1│ o │ o │ o │█a█│ b │ o │ e1│
├─────────┤ ┌─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┐
│child-set│───?│nul│nul│nul│nul│nul│NUL│nul│nul│nul│
└─────────┘ └───┴───┴───┴───┴───┴───┴───┴───┴───┘
┌─────────┐ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ key-set │───? │ s1│ o │ o │ o │█a█│ e │ b │ o │ e1│
├─────────┤ ┌─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┐
│child-set│───?│nul│nul│nul│nul│nul│NUL│ │nul│nul│nul│
└─────────┘ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
^
这里为理论上需要插入的新节点
*/
tips
参照这幅图,我们不难发现几个规律,只要我们将key向量稍稍挪位,就可以得到一个逻辑上很清晰的关系,在秩为r的小 节点上,其左孩子在child向量中的秩r,右孩子为r + 1
插入完成之后,我们会发现这个新的B树可能会跳出去,不再符合B树的定义,我们需要使用上溢
操作将其调整回来
┌───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┬───┬───┐
│400│470│500│ p->│400│470│500│ p->│400│440│470│500│
└───┼───┴───┤ └───┼───┴───┤ └───┼───┼───┴───┤
┌─────┘ └─┐ ┌───────┘ └─┐ ┌───┘ │ └─┐
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┐ ┌───┐ ┌───┬───┬───┐
│410│430│450│ │520│540│570│ v->│410│430│440│450│ │520│540│570│ v->│410│430│ u->│450│ │520│540│570│
└───┴───┴───┘ └───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┘ └───┴───┘ └───┘ └───┴───┴───┘
mid
┌────────────┐ ┌───────────┬───┐ ┌───────────┐
│ orgin tree │ │insert node│440│ │ overflow │
└────────────┘ └───────────┴───┘ └───────────┘
┌───┐
│470│
╳───╳
╱ ╲
╱─╱ ╲─────╲
╱ ╲
┌───┬───▼ ┌─▼─┐
│400│440│ │500│
└───┴───╳ └───╳
╱ ╲
╱────────╱ ╲
▼ ▼
┌───┬───┬───┐ ┌───┬───┬───┐
│410│430│450│ │520│540│570│
└───┴───┴───┘ └───┴───┴───┘
┌───────────┐
│ overflow │
└───────────┘
当节点发生上溢的时候,我们选取中位数,将其调整上去,然后将原来的节点分裂成俩部分
具体一点的方法就是:
对于删除来说,同样要好好利用search接口。和之前BST的删除方法一样,B树的节点真实孩子要么为0,要么一定大于等于2
因此对于这个删除来说,必须要依然保持BST的有序性,因此就要利用节点中序后继的做法
BTree<T>::remove(const T &e)
{
BTNodePos(T) v = search(e);
if(!v) return false;
--_size;
auto rIter = find(v->key.begin(), v->key.end(), e);
assert(rIter != v->key.end());
int r = rIter - v->key.begin();
在节点不是叶子节点的情况下,我们使用其中序后继,且其中序后继一定是叶子节点
if(v->child[0]) // 如果v不是叶子节点, 就交换其成为后继
{
BTNodePos(T) w = v->child[r + 1];
while(w->child[0] ) w = w->child[0]; // 此时w就是v的后继叶子节点
// 为什么? 因为在B树中,如果一个节点左子树为空,那么右子树一定为空。且其一定为叶子节点,这是定义
// 而一个节点的中序后继节点没有左子树,
v->key[r] = w->key[0];
v = w; r = 0;
} // 此时v处于最底层,删除v
v->key.erase(v->key.begin());
v->child.erase(v->child.begin() + r + 1);
solveUnderflow(v);
return true;
}
因此,同插入一样,所有的删除一定在最底层发生
对于删除来说,因为B树的关键码下限存在,所以可能会发生下溢情况,此时我们可以通过旋转和合并来解决
y x
┌───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┬───┐
│400│470│500│ P │400│470│500│ P │400│450│500│
└───┼───┼───┘ └───┼───┼───┘ └───┼───┼───┘
┌─────┘ └┐ ┌───────┘ └─┐ ┌───────┘ └──┐
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌───┬───┬───┐ ┌───┐ ┌───┬───┬───┐ ┌┐ ┌───┬───┐ ┌───┐
│410│430│450│ │490│ V│410│430│450│x U││ V│410│430│ U│470│y
└───┴───┴───┘ └───┘ └───┴───┴───┘ └┘ └───┴───┘ └───┘
┌────────────┐ ┌───────────┬───┐ ┌───────────┐
│ orgin tree │ │delete node│490│ │ rotate │
└────────────┘ └───────────┴───┘ └───────────┘
这是一颗2-4树,关键码的范围为1-3
我们发现V比较富裕,V > (m -1)/2 - 1 所以向V借一个节点,V也不会发生下溢,所以U借走了P中的y,P又
借走了V中的x,看起来像发生了旋转一样
┌───┬───┬───┐ ┌───┬───┬───┐ ┌───┐ ┌───┐ ┌───┬───┐
│400│470│500│ P│400│470│500│ P│400│ │500│ │400│500│
└───┼───┼───┘ └───┼───┼───┘ └───┤ ├───┘ └───┼───┘
┌─────┘ └┐ ┌───────┘ └─┐ ┌───────┘ └─┐ ┌──┘
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌───┐ ┌───┐ ┌───┐ ┌┐ ┌───┐ ┌───┐ ┌┐ ┌───┬───┐
│430│ │490│ V │430│ U││ V│430│ │470│U││ │430│450│
└───┘ └───┘ └───┘ └┘ └───┘ └───┘ └┘ └───┴───┘
┌────────────┐ ┌───────────┬───┐ ┌───────────┬───┐ ┌───────────┐
│ orgin tree │ │delete node│490│ │down angel │490│ │ merge │
└────────────┘ └───────────┴───┘ └───────────┴───┘ └───────────┘
依然是一颗2-4树,如果之前的V已经处于崩溃边缘的话,此时就没有兄弟借钱给U了,此时就需要天使融资了。
从P中下来一个天使,这个天使节点就是U和V中间共同的老爹节点。将V和U合并,我们发现P中就会消失一个节点,
自然有连续崩溃的可能性,因此同Overflow一样,依然需要递归处理,
┌───┐ ┌───┬───┐
│470│ │470│500│
├───┤ ├───┼───┘ │ │ │
┌─────┘ └┐ ┌───────┘ └─┐ ┌───────┘ └─┐ ┌──┘
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌───┐ ┌───┐ ┌───┐ ┌┐ ┌───┐ ┌───┐ ┌┐ ┌───┬───┐
│430│ │490│ │430│ ││ │430│ │470│ ││ │430│450│
└───┘ └───┘ └───┘ └┘ └───┘ └───┘ └┘ └───┴───┘
┌────────────┐ ┌───────────┬───┐ ┌───────────┬───┐ ┌───────────┐
│ orgin tree │ │delete node│490│ │down angel │490│ │ merge │
└────────────┘ └───────────┴───┘ └───────────┴───┘ └───────────┘
当天使节点来自根节点的时候,此时就会形成一个虚根,没有任何作用,自然直接处理掉这个虚根,
让根指向它的孩子节点。
具体的实际代码如下,
// 节点的下溢,做节点的旋转或者合并处理
template <typename T>
void
BTree<T>::solveUnderflow(BTNodePos(T) v)
{
if(v->child.size() >= (_order + 1) / 2) // _order + 1) / 2 就是向上取整的操作
return ; // 递归基:当前节点不满足下溢情况
BTNodePos(T) p = v->parent;
if(!p) // v已经是根节点,此时没有孩子的下限
{
if(!v->key.size() && v->child[0])
{
// 根节点没有关键码,却有非空孩子,此时应对根节点孩子发生合并的情况。可以直接跳过
_root = v->child[0];
_root->parent = nullptr;
v->child[0] = nullptr;
delete v;
}
return ;
}
size_t r = 0;
// 此时v中的目标节点已经被删除了,很可能不含有关键码
while(p->child[r] != v) r++; // 确定v是p的第几个孩子
// 情况1: 左顾,向左兄弟借码
if(r > 0) // 首先得有左兄弟
{
BTNodePos(T) ls = p->child[p->child.begin() + r - 1]; // v的左爹为y, ls的老幺为x
if(ls->child.size() > (_order + 1) / 2) // 必须大于下限
{
v->key.insert(v->key.begin(), p->key[r - 1]); // v向p借一个码y
p->key[r - 1] = ls->key[ls->key.size() - 1]; // p向ls借一个码x
v->child.insert(v->child.begin(), ls->child[ls->child.size() - 1]);//同时将x的右孩子过继给y
ls->key.erase(ls->key.end() - 1);
ls->child.erase(ls->child.end() - 1);
if(v->child[0]) v->child[0]->parent = v;
return ;
}
}
// 情况2: 右盼,向右兄弟借码
if(r < p->child.size() - 1)
{
BTNodePos(T) rs = p->child[p->child.begin() + r + 1]; // v的右爹为y,rs的老大为x
if(rs->child.size() > (_order + 1) / 2) // 右孩子足够胖,大于下限就足够胖
{
v->key.insert(v->key.end(), p->key[r]); //
p->key[r] = rs->key[0];
v->child.insert(v->child.end(), rs->child[0]); // 将x的左孩子过继给y
rs->key.erase(rs->key[0]);
if(rs->child[0])
rs->child[0]->parent = v;
rs->child.erase(rs->child[0]);
}
}
// 情况3: 左顾右盼失败,要么其没有左右兄弟(但不可能同时),要么左右兄弟太瘦,此时需要从上面下来一个天使, 合并
if(0 < r) // 和左兄弟合并,当然也可以先和右兄弟合并,随个人喜好
{
BTNodePos(T) ls = p->child[r - 1];
ls->key.push_back(p->key[r - 1]);
p->key.erase(p->key.begin() + r - 1);
ls->child.push_back(v->child[0]);
for(int i = 0; i < v->key.size(); ++i)
{
ls->key.push_back(v->key[i]);
ls->child.push_back(v->child[i+1]);
}
if(v->child[0])
{
for(int i = 0; i < v->child.size(); ++i)
{
v->child[i]->parent = ls;
delete v->child[i];
}
v->child.erase(v->child.begin(), v->child.end());
}
p->child.erase[p->child.begin() + r];
}
else// 和右兄弟合并
{
BTNodePos(T) rs = p->child[r + 1];
rs->key.push_back(p->key[r]); // 把天使先合并过来
p->key.erase(p->key.begin() + r); //
v->child.push_back(rs->child[0]);
for(int i = 0; i < rs->key.size(); ++i)
{
v->key.push_back(rs->key[i]);
v->child.push_back(rs->child[i+1]);
}
if(rs->child[0])
{
for(int i = 0; i < rs->child.size(); ++i)
{
rs->child[i]->parent = v;
delete rs->child[i];
}
rs->child.erase(rs->child.begin(), rs->child.end());
}
p->child.erase[p->child.begin() + r];
}
solveUnderflow(p);
return ;
}
对于B树来说,可能经常遇到它,一直不太明白它比红黑树优秀在哪里。现在明白其主要功能在于减少树的高度
从而减少IO次数。
对于B树来说,主要掌握的就是其插入与删除。首先要会自己画,明白有哪些情况,然后怎么处理。知道这些后
编程只是实现而已。
标签:只读 超级 parent 引用 核心 次数 二分查找 概述 结束
原文地址:https://www.cnblogs.com/patientcat/p/9720329.html