图解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. \(m\) 阶B+树表示了内部结点最多有 \(m-1\) 个关键字(或者说内部结点最多有 \(m\) 个子树), 阶数 \(m\) 同时限制了叶子结点最多存储 \(m-1\) 个记录.
  4. 内部结点中的key都按照从小到大的顺序排列, 对于内部结点中的一个key, 左树中的所有key都小于它, 右子树中的key都大于等于它. 叶子结点中的记录也按照key的大小排列.
  5. 每个叶子结点都存有相邻叶子结点的指针, 叶子结点本身依关键字的大小自小而大顺序链接.

B+ 树的插入操作

B+树的插入操作

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

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

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

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

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

  1. 中间索引节点的分裂与B树类似, 参考B树的分裂即可.
    因此我更新了B+树的分裂如果如下:
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的个数大于等于 \(\lceil \frac{m}{2} \rceil-1\), 删除操作结束,否则执行第2步.
  2. 若兄弟结点key有富余(大于 \(\lceil \frac{m}{2} \rceil-1\)) , 向兄弟结点借一个记录, 同时用借到的key替换父结(指当前结点和兄弟结点共同的父结点) 点中的key, 删除结束. 否则执行第3步.
  3. 若兄弟结点中没有富余的key,则当前结点和兄弟结点合并成一个新的叶子结点, 并删除父结点中的key(父结点中的这个key两边的孩子指针就变成了一个指针, 正好指向这个新的叶子结点), 将当前结点指向父结点(必为索引结点), 执行第4步(第4步以后的操作和B树就完全一样了, 主要是为了更新索引结点).
  4. 若索引结点的key的个数大于等于 \(\lceil \frac{m}{2} \rceil-1\), 则删除操作结束. 否则执行第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 @ 2024-07-30 11:24  虾野百鹤  阅读(32)  评论(0编辑  收藏  举报