Loading [MathJax]/jax/output/CommonHTML/fonts/TeX/AMS-Regular.js

图解B+树的插入与删除

B+树的定义#

上一篇我们介绍了B树, B+树与B树最大的不同是, B+树所有的关键字都存储在叶子节点, 中间节点仅作为索引.
关于B+树的定义以及解释要比B树多很多, 可能这也是因为B+树在实际使用中要比B树广泛很多. 我这里直接参考了nullzx对B+树的定义以及视图, 我主要修改我的B树的代码实现, 希望有用.

各种资料上B+树的定义各有不同, 一种定义方式是关键字个数和孩子结点个数相同. 这里我们采取维基百科上所定义的方式, 即关键字个数比孩子结点个数小1, 这种方式是和B树基本等价的. 上图就是一颗阶数为4的B+树.

除此之外B+树还有以下的要求.

  1. B+树包含2种类型的结点:内部结点(也称索引结点)和叶子结点. 根结点本身即可以是内部结点, 也可以是叶子结点. 根结点的关键字个数最少可以只有1个.
  2. B+树与B树最大的不同是内部结点不保存数据, 只用于索引, 所有数据(或者说记录)都保存在叶子结点中.
  3. mm 阶B+树表示了内部结点最多有 m1m1 个关键字(或者说内部结点最多有 mm 个子树), 阶数 mm 同时限制了叶子结点最多存储 m1m1 个记录.
  4. 内部结点中的key都按照从小到大的顺序排列, 对于内部结点中的一个key, 左树中的所有key都小于它, 右子树中的key都大于等于它. 叶子结点中的记录也按照key的大小排列.
  5. 每个叶子结点都存有相邻叶子结点的指针, 叶子结点本身依关键字的大小自小而大顺序链接.

B+ 树的插入操作#

B+树的插入操作

  1. 若为空树, 创建一个叶子结点, 然后将记录插入其中, 此时这个叶子结点也是根结点, 插入操作结束.

  2. 与B树相同, 插入操作都是从叶子节点开始的, 根据key值找到叶子结点, 向这个叶子结点插入记录. 插入后, 若当前结点key的个数小于等于 m1m1, 则插入结束. 否则
    a) 将这个叶子结点分裂成左右两个叶子结点, 左叶子结点包含前 m2m2 个记录, 右结点包含剩下的记录.
    b) 将第 m2+1m2+1 个记录的key进位到父结点中(父结点一定是索引类型结点), 进位到父结点的key左孩子指针向左结点,右孩子指针向右结点.
    c) 将左节点指向右节点(我的实现是将左节点最后一个孩子节点指向右节点)
    d) 父节点插入了一个节点, 将当前结点的指针指向父结点, 然后执行第3步.

  3. 针对索引类型结点:如果叶子节点发生分裂, 会在索引节点中插入关键字. 若当前索引结点key的个数小于等于 m1m1, 则插入结束.
    a) 将这个索引类型结点分裂成两个索引结点, 左索引结点包含前 m12key, 右结点包含 mm12个keys. 注意中间节点实际上被删除了, 移动到父节点中.
    b) 将第 m2个key进位到父结点中, 进位到父结点的key左孩子指向左结点, 进位到父结点的key右孩子指向右结点. 将当前结点的指针指向父结点, 然后重复第3步.

下面是一颗5阶B树的插入过程, 5阶B数的结点最少2个key, 最多4个key.

  1. 叶子节点的分裂与B树有所不同, 叶子节点关键字个数到达5个之后分裂, 分裂后左节点2个关键字, 右节点(新建节点)三个关键字, 没有关键字上移, 但是新增了左节点指向右节点的指针.

  1. 中间索引节点的分裂与B树类似, 参考B树的分裂即可.
    因此我更新了B+树的分裂如果如下:
Copy
template <class T, int Order> Node<T, Order>* Node<T, Order>::split(Node* node, T* med) //mid to store value of mid and use it in insert func { int NumberOfKeys = node->NumbersOfKeys; Node<T,Order> *newNode = nullptr; int midValue = NumberOfKeys / 2; // 返回med的值, 上移到父节点 *med = node->keys[midValue]; int node_pos; // 如果是叶子节点分裂, 那么中间节点需要拷贝到新建的右节点中 if(node->leaf_node) { newNode = new Node<T, Order>(true); node_pos = midValue; } else { newNode = new Node<T, Order>(false); node_pos = midValue+1; } //take the values after mid value while (node_pos<NumberOfKeys) { newNode->keys[++newNode->position] = node->keys[node_pos]; newNode->childs[newNode->position] = node->childs[node_pos]; ++newNode->NumbersOfKeys; --node->position; --node->NumbersOfKeys; node->keys[node_pos] = 0; node->childs[node_pos] = nullptr; node_pos++; } // 孩子节点的个数要比Key的个数多一个 newNode->childs[newNode->position + 1] = node->childs[node_pos]; node->childs[node_pos] = nullptr; // 如果是中间节点分裂, 节点向上移动, 这一层减少一个节点 if(!node->leaf_node) { --node->NumbersOfKeys; //because we take mid value... --node->position; } else { // 如果是叶子节点分裂, 将左节点的最后一个孩子的指针指向右节点 node->childs[Order] = newNode; } return newNode; }

B+树的删除操作#

如果叶子结点中没有相应的key, 则删除失败. 否则执行下面的步骤

  1. 删除叶子结点中对应的key. 删除后若结点的key的个数大于等于 m21, 删除操作结束,否则执行第2步.
  2. 若兄弟结点key有富余(大于 m21) , 向兄弟结点借一个记录, 同时用借到的key替换父结(指当前结点和兄弟结点共同的父结点) 点中的key, 删除结束. 否则执行第3步.
  3. 若兄弟结点中没有富余的key,则当前结点和兄弟结点合并成一个新的叶子结点, 并删除父结点中的key(父结点中的这个key两边的孩子指针就变成了一个指针, 正好指向这个新的叶子结点), 将当前结点指向父结点(必为索引结点), 执行第4步(第4步以后的操作和B树就完全一样了, 主要是为了更新索引结点).
  4. 若索引结点的key的个数大于等于 m21, 则删除操作结束. 否则执行第5步
  5. 若兄弟结点有富余, 父结点key下移, 兄弟结点key上移, 删除结束. 否则执行第6步
  6. 当前结点和兄弟结点及父结点下移key合并成一个新的结点. 将当前结点指向父结点, 重复第4步.

注意, 通过B+树的删除操作后, 索引结点中存在的key, 不一定在叶子结点中存在对应的记录.

下面是一颗5阶B树的删除过程, 5阶B数的结点最少2个key, 最多4个key.

B+树的删除与B树删除的不同点主要体现在叶子节点的处理, 叶子节点不满足条件时, 存在两种处理方式:

  1. 向兄弟节点借一个关键字: 不同于B树, 向兄弟节点接关键字涉及到父节点下移动, 兄弟节点替换父节点. 实际B树更像是AVL平衡树的移动方式. 而B+树的叶子节点, 是真的向兄弟节点借一个关键字, 兄弟节点移动, 并且将一个关键字替换父节点的关键字. (因为父节点是索引节点).
  2. 与兄弟节点合并: 兄弟节点的合并与B树类似, 但是需要注意叶子节点之间的指针.

    B+树的删除也和B树一样会自下向上, 到索引节点时, 删除的方式就和B树完全一样了.

B+树插入与删除的实现#

我将B+树插入与删除的实现记录在了我的Github中, 传送门. B+树是基于B树实现的, 在B树的基础上修改即可.

posted @   虾野百鹤  阅读(410)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 我干了两个月的大项目,开源了!
· 推荐一款非常好用的在线 SSH 管理工具
· 千万级的大表,如何做性能调优?
· 盘点!HelloGitHub 年度热门开源项目
· Phi小模型开发教程:用C#开发本地部署AI聊天工具,只需CPU,不需要GPU,3G内存就可以运行,
点击右上角即可分享
微信分享提示
CONTENTS