B-tree

一、B树的性质

一颗M阶B树T,满足以下条件
1. 每个结点至多拥有M课子树;
2. 根结点至少拥有两颗子树;
3. 除了根结点以外,其余每个分支结点至少拥有M/2课子树;
4. 所有的叶结点都在同一层上;
5. 有k课子树的分支结点则存在k-1个关键字,关键字按照递增顺序进行排序;
6. 关键字数量满足ceil(M/2)-1 <= n <= M-1。

例子:关键字为26英文字母,阶数M为6,按顺序插入得到的B-tree如下所示。

b 树java实现 b树c++实现_算法

二、B树的创建

2.1 节点的定义

/**
 * B-tree
 * create by marco
 **/
#include <iostream>
#include<vector>
using namespace std;

typedef int KEY_VALUE;

/* 节点数据结构 */
struct btree_node 
{
    KEY_VALUE *keys;    // 节点中的关键字数组
    struct btree_node **children;   // 指向子节点的数组
    int num;    // 节点中关键字个数
    int leaf;   // 是否是叶子节点
};

/* 创建B树节点 */
btree_node* btree_node_create(int dgree, int leaf) 
{
    btree_node *node = (btree_node*)calloc(1, sizeof(btree_node));
    if (node == nullptr) {
        printf("btree_node_create failed!\n");
        return nullptr;
    }

    node->leaf = leaf;
    node->keys = (KEY_VALUE*)calloc(1, (dgree * 2 - 1) * sizeof(KEY_VALUE));
    node->children = (btree_node**)calloc(1, (dgree * 2) * sizeof   (btree_node));
    node->num = 0;
    return node;
}

/* 释放B树节点 */
void btree_node_destory(btree_node *node)
{
    if (node == nullptr)
        return;
    free(node->children);
    free(node->keys);
    free(node);
}

2.2 B树的定义

struct btree 
{
    struct btree_node *root;    // 指向B树的根节点
    int dgree;  // B树的阶数的一半, 如此设置方便之后判断节点是否需要操作
};

/* 创建B树 */
btree* btree_create(int dgree)
{
    btree *T = new btree;

    T->dgree = dgree;
    btree_node *node = btree_node_create(dgree, 1);
    T->root = node;

    return T;
}

三、B树的插入操作

3.1 B树插入关键字的主要思想

  1. 首先二叉遍历找到待插入的叶子节点。注意,B树关键字的插入,必然是插入到叶子节点,可以这么理解,不遍历到叶子节点是无法判断关键字是否已存在的。
  2. 判断叶子节点的关键字个数是否小于阶数M-1,若小于即可直接判断插入的具体位置。插入关键字B、C、D如下图所示。
  3. b 树java实现 b树c++实现_b 树java实现_02

  4. 如果关键字个数等于阶数M-1,则表示该节点已满,需要分裂节点。分裂叶子节点的具体规则为:将中间关键字(索引为阶数M/2 - 1)放入父节点,新建节点存入被分裂叶子节点的后一半数据,并将新建节点插入到父节点中,排在被分裂叶子节点的分支之后。记得更新被分裂的叶子节点,已经被瓜分啦。插入关键字F如下图所示。
  5. b 树java实现 b树c++实现_算法_03

  6. 注意此时需要递归判断父节点的关键字个数,如果等于阶数M-1,则需继续分裂。
  7. 分裂结束即可插入关键字。

3.2 具体实现代码如下

/* 分裂B树节点 */
/**
 * 参数列表:
 *      btree *T : B树指针
 *      btree_node *x : 待分裂节点的父节点指针
 *      int index : 待分裂节点在父节点中的孩子指针索引
 * */
void btree_node_split(btree *T, btree_node *x, int index)
{
    int dgree = T->dgree;
    btree_node * y = x->children[index];

    /* 新建节点z,将待分裂节点的后一半关键字放入新节点z */
    btree_node *z = btree_node_create(dgree, y->leaf); // 创建节点,传入参  数为:节点的阶数+是否叶子节点
    z->num = dgree - 1; // 设置新节点z的关键字个数
    for (int k = dgree; k < 2 * dgree - 1; ++k)
    {
        z->keys[k - dgree] = y->keys[k];
    }
    if (y->leaf == 0)   // 若待分裂节点为非叶子节点,则拥有的子树也需要复制到   新节点z
    {
        for (int k = dgree; k < 2 * dgree; ++k )
        {
            z->children[k - dgree] = y->children[k];
        }
    }

    /* 将分裂出的关键字放入父节点x */
    for (int k = x->num - 1; k >= index; k--)   // 将后半关键字往后移动位   置,腾出空间,以便存放分裂节点的中间关键字
    {
        x->keys[k + 1] = x->keys[k];
    }
    x->keys[index] = y->keys[dgree - 1]; 
    x->num += 1;
    y->num = dgree - 1;

    /* 将新建节点z作为子树插入到x节点 */
    for (int k = x->num; k > index; --k)    // 将父节点中部分子树往后移动一 个单位,以便插入新建节点z
    {
        x->children[k + 1] = x->children[k];
    }  
    x->children[index + 1] = z;
}

/* 插入关键字---根节点非满 */
void btree_insert_key_nodeNonFull(btree *T, btree_node *x, KEY_VALUE key)
{
    int index = x->num - 1;

    if (x->leaf == 1) 
    {
        while (index >= 0 && x->keys[index] > key )
        {
            x->keys[index + 1] = x->keys[index];
            index--;
        }
        x->keys[index + 1] = key;
        x->num += 1;
    }
    else    // 非叶子节点,则该节点必然不会是待插入节点,继续往下探索,探索过程中判断即将探索的下层节点关键字是否满,满了需要分裂
    {
        while (index >= 0 && x->keys[index] > key)
            index--;

        if (x->children[index + 1]->num == (2 * T->dgree - 1))
        {
            btree_node_split(T, x, index + 1);
            if (x->keys[index + 1] < key)   // 更新待探索的子节点索引,因为 分裂节点会向x节点插入新的关键字,所以要重新判断该关键字是否小于key
                index++;
        }

        btree_insert_key_nodeNonFull(T, x->children[index + 1], key);
    }
}

/* 插入关键字 */
void btree_insert_key(btree *T, KEY_VALUE key)
{
    btree_node *r = T->root;

    if (r->num == 2 * T->dgree - 1) // 特殊情况,根节点关键字满,需要分裂并且   重新设置根节点
    {
        btree_node *node = btree_node_create(T->dgree, 0);
        T->root = node;
        node->children[0] = r;
        btree_node_split(T, node, 0);

        int i = 0;
        if (node->keys[0] < key)    // 判断节点插入的位置
            ++i;
        btree_insert_key_nodeNonFull(T, node->children[i], key);
    }
    else
    {
        btree_insert_key_nodeNonFull(T, r, key);
    }
}

四、B树的删除操作

4.1 B树删除关键字的主要思想

  1. 首先二叉遍历找到关键字所在的节点。
  2. 判断节点的关键字个数删除之后是否满足:ceil(M/2)-1 <= n <= M-1。满足即可直接删除啦。删除关键字A如下如所示。
  3. b 树java实现 b树c++实现_数据结构_04

  4. 不满足的话,首先判断是否可以向左右兄弟节点借关键字。判断规则:a.左右兄弟节点关键字被借走一个之后个数n仍然满足ceil(M/2)-1 <= n <= M-1;b.当左右兄弟都满足时,选关键字个数多的兄弟节点。
  5. 具体借的规则:首先向父节点借一个左/右关键字(与借的节点方位保持一致)放入待删除关键字的节点中,然后将左/右兄弟节点中的最大/最小关键字复制到父节点中。相应的孩子指针也要相应的移动。删除关键字S如下图所示。
  6. b 树java实现 b树c++实现_算法_05

  7. 如果左右子节点都无法借出关键字时,需要进行合并操作,即将左/右兄弟节点和当前待删除关键字的节点,以及最后一个小于该关键字的父节点中的关键字合并。删除关键字M如下图所示。
  8. b 树java实现 b树c++实现_b树_06

  9. 每个节点中相应的孩子指针的实际操作和关键字类似。

4.2 具体实现代码如下

/* 合并节点 */
/**
 * 参数列表:
 *      btree *T : B树指针
 *      btree_node *node : 待合并节点的上一次父节点指针
 *      int index : 合并需要三个部分---父节点关键字,父节点关键字前后的子节点。 index为父节点关键字索引
 * */
void btree_node_merge(btree *T, btree_node *node, int index)
{
    btree_node *left = node->children[index];   // 合并的左子节点
    btree_node *right = node->children[index + 1];  // 合并的右子节点

    /* 将父节点关键字以及右子节点都存入左子节点中 */
    left->keys[T->dgree - 1] = node->keys[index];
    for (int i = 0; i < right->num; ++i)    // 移动right中的关键字
    {
        left->keys[T->dgree + i] = right->keys[i];
    }
    if (left->leaf == 0)    // 移动right中的子节点指针
    {
        for (int i = 0; i <= right->num; ++i)
        {
            left->children[T->dgree + i] = right->children[i];
        }
    }
    left->num += right->num + 1;

    btree_node_destory(right);  // 释放右子节点

    for (int i = index + 1; i < node->num; ++i) // 更新父节点关键字和指向子 节点的指针位置
    {
        node->keys[i - 1] = node->keys[i];
        node->children[i] = node->children[i + 1];
    }
    node->children[node->num + 1] = nullptr;
    node->num -= 1;

    if (node->num == 0) // 如果父节点个数为0,这种情况只可能是父节点为根节点
    {
        T->root = left;
        btree_node_destory(node);
    }
}

/* 删除节点中关键字 */
void btree_node_delete_key(btree *T, btree_node *node, KEY_VALUE key)
{
    if (node == nullptr)
        return;

    int index = 0;
    // 找到节点中第一个大于等于key的关键字
    while (index < node->num && key > node->keys[index])    
        index++;

    if (index < node->num && node->keys[index] == key) {    // 在该节点中找 到等于key的关键字, 考虑具体的删除操作

        if (node->leaf == 1)    // 该节点为叶子节点
        {
            for (int i = index; i < node->num - 1; ++i)
            {
                node->keys[i] = node->keys[i + 1];
            }
            node->keys[node->num - 1] = 0;
            node->num -= 1;

            if (node->num == 0)
            {
                free(node);
                T->root = nullptr;
            }
        } else if (node->children[index]->num >= T->dgree) {    // 借用左   子节点的关键字进行覆盖
            btree_node * left = node->children[index];
            node->keys[index] = left->keys[left->num - 1];

            btree_node_delete_key(T, left, left->keys[left->num - 1]);  /   / 递归删除子节点中用来覆盖上层节点的关键字
        } else if (node->children[index + 1]->num >= T->dgree) {    // 借   用左子节点的关键字进行覆盖
            btree_node *right = node->children[index + 1];
            node->keys[index] = right->keys[0];

            btree_node_delete_key(T, right, right->keys[0]);    // 递归删   除子节点中用来覆盖上层节点的关键字
        } else {
            btree_node_merge(T, node, index);
            btree_node_delete_key(T, node->children[index], key);
        }

    } else {    // 继续往children[index]子节点找

        btree_node *child = node->children[index];
        if (child == nullptr) {
            printf("Cannot find key : %d\n", key);
            return;
        }

        /* 找关键字所在的节点过程中,遇到关键字个数==dgree-1的节点,进行丰满处  理 */
        if (child->num == T->dgree - 1) {
            btree_node *left = nullptr, *right = nullptr;
            if (index > 0)
                left = node->children[index - 1];
            if (index < node->num)
                right = node->children[index + 1];

            if ((left && left->num >= T->dgree) || (right && right->num     >= T->dgree)) {
                int select = 0;
                if (left && right)
                    select = (right->num > left->num) ? 1 : 0;

                if (select) {   // 选右子树
                    /* 将父节点node的第一个大于key的关键字移给child */
                    child->keys[child->num] = node->keys[index];
                    child->num += 1;
                    child->children[child->num] = right->children[0];

                    /* 将child右兄弟节点的第一个关键字移给父节点node */
                    node->keys[index] = right->keys[0];

                    /* 更新child右兄弟节点中关键字和子节点指针的位置 */
                    for (int i = 0; i < right->num - 1; ++i) {
                        right->keys[i] = right->keys[i + 1];
                        right->children[i] = right->children[i + 1];
                    }
                    right->keys[right->num - 1] = 0;
                    right->children[right->num - 1] = right->children   [right->num];
                    right->children[right->num] = nullptr;
                    right->num -= 1;

                } else {    // 选左子树

                    child->num++;
                    child->children[child->num] = child->children   [child->num - 1];
                    for (int i = child->num - 2; i >= 0; --i) {
                        child->keys[i + 1] = child->keys[i];
                        child->children[i + 1] = child->children[i];
                    }
                    child->keys[0] = node->keys[index - 1];
                    child->children[0] = left->children[left->num];

                    node->keys[index - 1] = left->keys[left->num - 1];

                    left->keys[left->num - 1] = 0;
                    left->children[left->num] = nullptr;
                    left->num -= 1;
                }
            } else {
                if (left && left->num == T->dgree - 1) {
                    btree_node_merge(T, node, index - 1);
                    child = left;
                } else if (right && right->num == T->dgree - 1) {
                    btree_node_merge(T, node, index);
                }
            }
        }
        btree_node_delete_key(T, child, key);
    }
}


/* 删除B树关键字 */
void btree_delete_key(btree *T, KEY_VALUE key) {
    if (T->root == nullptr)
        return ;

    btree_node_delete_key(T, T->root, key);
}

五、B树的应用案例

案例一、设计一个图片存储索引组件
  1. 图片的数量是巨大的。
  2. 可以添加图片,可以删除图片
  3. 可以通过图片名称进行查找
  4. 添加,删除,查找都是对单一图片进行操作

六、Tips

下面代码为打印函数和主函数,所有代码段可组成完整的B树创建、添加、删除代码。

void btree_print(btree *T, btree_node *node, int layer)
{
    btree_node *p = node;

    if (p) {
        printf("\nlayer = %d keynum = %d is_leaf = %d\n", layer, p->num, p->leaf);
        for(int i = 0; i < node->num; i++)
            printf("%C ", p->keys[i]);
        printf("\n");

        layer++;
        for(int i = 0; i <= p->num; i++)
            if(p->children[i])
                btree_print(T, p->children[i], layer);
    }
    else 
        printf("the tree is empty\n");
}

int main() {
    btree *T = btree_create(3);

    char key[27] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    for (int i = 0; i < 26; i++) {
        printf("%C ", key[i]);
        btree_insert_key(T, key[i]);
        btree_print(T, T->root, 0);
    }

    btree_print(T, T->root, 0);

    for (int i = 0; i < 26; i++) {
        printf("\n---------------------------------\n");
        btree_delete_key(T, key[25-i]);
        btree_print(T, T->root, 0);
    }

    return 0;
}

七、 源代码下载