B+树插入
B+树定义
B+树是一种多路平衡查找树,多用于文件系统中当作索引。对于B+树网上也有好多不同的定义,在使用时大家可以根据实际需求做些许改动,我就采用如下方式:
- 树中每个结点最多有M棵子树,同时最多只有M-1个关键字。
- 若根节点不是叶子节点则至少有两棵子树。
- 除根节点外,其它所有非叶子节点至少有⌈M/2⌉棵子树。
- 所有节点上的关键字均已排序(假设为递增序列),且关键字左边子树的值均小于该关键字的值,而右边子树的值大于等于其值。
- 除本层最后一个关键字外,其它关键字必须有左、右两个孩子节点。
- 所有叶子节点都在同一层上,且包含所有信息。
- 非叶子节点上的关键字只充当索引并不代表任何信息。
插入操作
B+树的插入操作主要分几类讨论,并不是太复杂,通过一个示例很容易理解这个过程。我们就依次插入(24, 29, 2, 17,5, 19,3, 18, 7, 11, 37,15,23, 31)这些数据,一步步构建一棵B+树。
- 一开始只有一个根节点直接插入数据即可:
- 接着插入29和2,只需注意排序就行:
- 当插入17时,会发现当前节点已满,此时需要将该节点分列成两个节点,并再生成一个节点做为新的根节点:
- 插入5就是直接往2,17对应节点放就行。但当要插入19时,发现该节点满了,就采用与上面同样的操作进行分裂,并把中间元素17调至根节点:
- 按同样的方法插入3,18,7,11,37得到:
- 插入15是比较麻烦的一步,也容易出错。通过查找能够知道15应该插入到第二个叶子节点,此时该节点已有3个关键字,则按照之前第3步插入17一样进行分裂,然后把11提到父节点中。但这时我们会发现其父节点也已经满了,那么就需要进行再一次分裂,把17提到新的根节点去,但是要注意在非叶子节点分裂时,向上提的数17不能在当前层节点中出现(这一步是最容易出错的),因为不这样就无法满足定义中的4和5:
- 最后的23和31的插入就简单了,就不再累述:
实现
下面给出C++的代码
template <typename T, int M = 4> //M为阶数
class BPTree
{
private:
BPNode* root; //根节点
BPNode* firstLeaf; //第一个叶子节点
BPNode* lastLeaf; //最后一个叶子节点
public:
struct BPNode
{
T keys[M]; //关键字
BPNode* nodes[M + 1]; //孩子节点指针
BPNode* pre, *next, *parent; //节点中包含父节点指针,且叶子节点用双链表连接
int nKey; //关键字个数,关键字和孩子节点数组都多开辟了一个空间方便插入
BPNode()
{
nKey = 0;
pre = next = parent = 0;
for (int i = 0; i < M + 1; i++)
nodes[i] = 0;
}
};
public:
BPTree()
{
root = new BPNode;
firstLeaf = lastLeaf = root;
}
bool IsEmpty()
{
return root->nKey == 0;
}
void Insert(const T& x)
{
if (IsEmpty()) //插入第一个数
{
root->keys[0] = x;
root->nKey++;
return;
}
BPNode* p = root;
while (p->nodes[0]) //寻找插入数据的节点
{
int i;
for (i = 0; i < p->nKey; i++)
{
if (x < p->keys[i])
break;
if (x == p->keys[i])
return;
}
p = p->nodes[i];
}
__Insert(p, x); //因为一开始多开辟了空间就不管节点是否满直接插入数据
if (p->nKey < M) //若插入后节点未溢出就完成插入,否则进行分裂
return;
BPNode *left, *right;
T midKey;
__SplitNode(p, left, right, midKey, true); //将p分裂成两个节点
//将叶子节点用双链表连接起来
right->next = left->next;
left->next = right;
right->pre = left;
if (right->next)
right->next->pre = right;
else
lastLeaf = right;
BPNode *pp = p->parent;
int pos = __Insert(pp, midKey); //midKey是要向上提的元素,将其插入到父节点
left->parent = pp;
right->parent = pp;
pp->nodes[pos] = left;
pp->nodes[pos + 1] = right;
__AdjustParent(pp); //父节点中插入midKey后还需要对父节点进行调整
}
private:
int __Insert(BPNode*& p, const T& x)
{
if (!p) //第一次插入或根节点分裂产生新的根节点
{
p = new BPNode;
root = p;
}
int pos = 0;
while (pos < p->nKey && x >= p->keys[pos]) //寻找插入位置
{
if (x == p->keys[pos])
return -1;
pos++;
}
for (int j = p->nKey; j > pos; j--) //将插入位置后面的元素向后移动
{
p->keys[j] = p->keys[j - 1];
p->nodes[j + 1] = p->nodes[j];
}
//插入数据
p->keys[pos] = x;
p->nKey++;
return pos;
}
//把节点p分裂成left和right两个节点,midKey用来返回上提的元素,containMid表示右节点中是否需要包含上提元素
void __SplitNode(BPNode* p, BPNode*& left, BPNode*& right, T& midKey, bool containMid)
{
int mid = p->nKey / 2;
midKey = p->keys[mid];
right = new BPNode;
left = p;
int i, j;
for (i = mid, j = 0; i < p->nKey; i++) //mid以后的节点复制到right节点中
{
if (!containMid && i == mid)
continue;
right->keys[j] = p->keys[i];
right->nodes[j] = p->nodes[i];
right->nKey++;
j++;
}
right->nodes[j] = p->nodes[i];
left->nKey = mid; //左节点中只修改nKey的值不需要真正删除元素
}
void __AdjustParent(BPNode* p)
{
if (!p || p->nKey < M) //p节点中未溢出则直接完成插入
return;
BPNode *left, *right;
T midKey;
__SplitNode(p, left, right, midKey, false);
BPNode* pp = p->parent;
int pos = __Insert(pp, midKey);
pp->nodes[pos] = left;
pp->nodes[pos + 1] = right;
//分裂后注意修改right节点的孩子节点的父指针
for (int i = 0; i <= right->nKey; i++)
right->nodes[i]->parent = right;
left->parent = right->parent = pp;
__AdjustParent(pp); //上提元素后,继续递归调整父节点直到没有溢出为止
}
};
总结
这是第一次接触较复杂的数据结构,由于能力有限,感觉完成插入代码还是挺费劲的,如有错误希望大家指正,谢谢。
作者: 只短浅长