有重复值的B+树的插入,删除,单点查找,区间查找,全纪录检索

有重复值的B+树的插入,删除,单点查找,区间查找,全纪录检索

B+树的由来

B+树是在B树的基础上加以改进的,如果还有对B树不熟悉的同学,可以先看看这篇 B树
在这里插入图片描述

B树有两个缺点

  1. 区间查找不好实现,如果我要找[31,55]之间的数,他需要把区间查找退化为单点查找,复杂度很高

  2. 全纪录检索不好实现,输出所有值,也就相当于第一个区间查找[5,65]的数

由此,得出B+树

在这里插入图片描述

其中,只有叶子结点才是真正存key-value的地方,非叶子结点只是作为检索,只存key

注意:所有的叶子结点链接到一起组成一个双向链表,便于进行全纪录检索和区间查询

B+树的基本规则

在这里插入图片描述图片上有点小错误,最多含n-1哥关键字

最少含有的关键字是n/2

B+树的核心是平衡"、“多路”,通过限制非叶子节点的孩子数量,B树可以保持在相对平衡的状态,避免节点过于分散或过于拥挤。这种平衡性有助于提高搜索、插入和删除操作的性能;而且可以提高存储利用率,降低树的高度。
举个例子如果是3阶,那最少就是1,这样可以保证树的深度不会太深

含有重复关键字的B+树

  • 我们上述说的B+树有个条件,即所有关键字不能重复

  • 但B+树是MySQL等数据库的底层实现,在插入一条数据时,一定会有重复的key,此时如果进行数据检索的话,应该如何来做呢?

  • 其实很简单:在叶子结点的每一条key对应的value,不是存放单个value,而是设置一个链表,如果有相同的key的value就放在一起

    此处可以联想一下哈希表中散列后具有相同地址的如何处理的(链地址法)

    在这里插入图片描述

故,含有重复关键字的B+树可以设计成链表形式

在这里插入图片描述

如图上所示,每一个key存储着一个链表的value

含有重复关键字的B+树的插入,删除,单点查找,区间查找,全纪录检索思路

有重复关键字的B+树的操作宇原来B+树操作类似,不过就是新开辟了一个链表

B+树的插入,举例说明

这是一个5阶b+树
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

总结来说:

在向上分裂过程中分为两种情况,叶子节点分裂和非叶子节点分裂
(这个下面的举例是一个节点有两个指针的)
在这里插入图片描述
在这里插入图片描述

核心思路:在把6插入父节点后,需要再次检查父节点是否超过最大节点,如果超过,再次执行该操作,到根节点后,若根节点仍超过最大节点,则需要新开辟一个节点当根

删除操作

  • 定位关键字所在的叶子节点

    • 使用 B+ 树的搜索功能,从根节点开始向下查找,定位包含关键字的叶子节点。
    • 删除关键字或记录

      • 叶子节点

        • 在叶子节点的 keys[] 数组中找到关键字的位置并移除它。
        • 如果有重复值链表,仅删除匹配的记录节点。
        • 删除后,调整 keys[]pointers[] 以保持数组紧凑。
  • 非叶子节点

    • 删除的关键字可能存在于父节点中,且只起到索引作用。父节点中的索引无需立即删除,除非合并发生。
  • 检查节点是否不平衡

    • 判断删除后叶子节点的关键字数量是否小于

      Min_KeysMin\_KeysMin_Keys

      • 如果不小于,删除操作完成。
    • 如果小于,需要通过借用或合并恢复平衡。

  • 借用(Redistribution)

    • 尝试从兄弟节点借用一个关键字。

    • 如果左兄弟节点存在并且有多于 Min_KeysMin\_KeysMin_Keys 个关键字:

      • 借用左兄弟的最大关键字,并更新父节点中的索引。
    • 如果右兄弟节点存在并且有多于

      Min_KeysMin\_KeysMin_Keys个关键字:

      • 借用右兄弟的最小关键字,并更新父节点中的索引。
  • 合并(Merge)

    • 如果无法借用关键字,需要将当前节点与其兄弟节点合并。

    • 合并操作:

      1. 将当前节点和兄弟节点的关键字合并到一个节点中。
      2. 如果是叶子节点,保持 next 指针的连续性。
      3. 删除父节点中的索引,并更新指针。
    • 如果合并后导致父节点关键字数量不足,递归向上调整。

举个例子:
这个也是对应一个节点有两个分叉点的,和我们刚刚讲的不太一样
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

删除的核心:

  1. 保持平衡:借用和合并确保所有节点的关键字数量不低于 Min_KeysMin_KeysMin_Keys。

  2. 保持顺序性:合并和调整索引时,关键字顺序保持不变。

  3. 高度递归调整:删除可能从叶子节点递归调整到根节点。

单点查找

在这里插入图片描述

举例

比如说我要找下图中的11

在这里插入图片描述

  1. 查找第一个大于要找的key值,从根节点往下找,先找到22,
  2. 再进入22所指的下一层,查找第一个大于要找的key值,即13
  3. 再进入13所指的下一层,找到了等于11的节点,再检查是不是叶子结点(因为非叶子结点只存key,作为查找的标识符,叶子节点才找value),发现是叶子节点
  4. 然后找到他指向的一个链表所对应的value

区间查找

在这里插入图片描述

我要查找[13,33]之内的所有数,直接遍历链表,找到两个边界就可以

全纪录检索

在这里插入图片描述

  • 就是从head头指针开始遍历,一直到尾节点,即为全纪录检索

代码

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define ORDER 4 // B+树的阶数,表示每个节点最多包含ORDER个指针

// 数据节点,用于存储叶子节点的值
typedef struct Record {
    int value;           // 数据值
    struct Record *next; // 链表,用于存储重复关键字的多个值
} Record;

// 树的节点
typedef struct Node {
    bool is_leaf;         // 是否是叶子节点
    int num_keys;         // 当前关键字数量
    int keys[ORDER - 1];  // 关键字数组
    void *pointers[ORDER];// 指针数组,指向子节点或记录
    struct Node *next;    // 用于叶子节点的链表
} Node;

// B+树结构
typedef struct BPlusTree {
    Node *root; // 树的根节点
} BPlusTree;

// 创建一个记录
Record *create_record(int value) {
    Record *new_record = (Record *)malloc(sizeof(Record));
    if (new_record == NULL) {
        perror("OverFlow");
        exit(EXIT_FAILURE);
    }
    new_record->value = value;
    new_record->next = NULL;
    return new_record;
}

// 创建一个节点
Node *create_node(bool is_leaf) {
    Node *new_node = (Node *)malloc(sizeof(Node));
    if (new_node == NULL) {
        perror("Failed to create node");
        exit(EXIT_FAILURE);
    }
    new_node->is_leaf = is_leaf;
    new_node->num_keys = 0;
    new_node->next = NULL;
    for (int i = 0; i < ORDER; i++) {
        new_node->pointers[i] = NULL;
    }
    /*
    对于叶子节点,pointers[i] 指向存储数据的链表记录。
	对于非叶子节点,pointers[i] 指向子节点。
    */
    return new_node;
}

// 初始化B+树
BPlusTree *initialize_tree() {
    BPlusTree *tree = (BPlusTree *)malloc(sizeof(BPlusTree));
    if (tree == NULL) {
        perror("Failed to initialize tree");
        exit(EXIT_FAILURE);
    }
    tree->root = create_node(true); // 初始时根节点是叶子节点
    return tree;
}

// 在叶子节点插入记录
void insert_into_leaf(Node *leaf, int key, Record *record) {
    int i;
    for (i = leaf->num_keys - 1; i >= 0 && leaf->keys[i] > key; i--) {
        leaf->keys[i + 1] = leaf->keys[i];
        leaf->pointers[i + 1] = leaf->pointers[i];
    }
    if (i >= 0 && leaf->keys[i] == key) {
        // 如果关键字已存在,追加到记录链表
        Record *existing_record = (Record *)leaf->pointers[i];
        while (existing_record->next != NULL) {
            existing_record = existing_record->next;
        }
        existing_record->next = record;
    } else {
        // 插入新的关键字和记录
        leaf->keys[i + 1] = key;
        leaf->pointers[i + 1] = record;
        leaf->num_keys++;
    }
}

void insert(BPlusTree *tree, int key, int value) {
    Node *root = tree->root;
    Node *current = root;
    Node *parent = NULL; // 用于追踪父节点

    // 1. 寻找插入的叶子节点
    while (!current->is_leaf) {
        parent = current;
        int i = 0;
        while (i < current->num_keys && key >= current->keys[i]) {
            i++;
        }
        current = (Node *)current->pointers[i];
    }

    // 2. 插入记录到找到的叶子节点
    insert_into_leaf(current, key, create_record(value));

    // 3. 检查节点是否需要分裂
    while (current->num_keys == ORDER) { // 如果节点的关键字数量超出限制
        Node *new_node = create_node(current->is_leaf); // 新节点,用于存储分裂出的关键字

        // 4. 分裂逻辑
        int mid = current->num_keys / 2; // 分裂点
        if (current->is_leaf) {
            // 如果是叶子节点,将后一半的关键字移到新节点
            new_node->num_keys = current->num_keys - mid;
            for (int i = 0; i < new_node->num_keys; i++) {
                new_node->keys[i] = current->keys[mid + i];
                new_node->pointers[i] = current->pointers[mid + i];
            }
            current->num_keys = mid;

            // 链接叶子节点
            new_node->next = current->next;
            current->next = new_node;
        } else {
            // 如果是内部节点,将后一半的关键字和指针移到新节点
            new_node->num_keys = current->num_keys - mid - 1;
            for (int i = 0; i < new_node->num_keys; i++) {
                new_node->keys[i] = current->keys[mid + 1 + i];
                new_node->pointers[i] = current->pointers[mid + 1 + i];
            }
            new_node->pointers[new_node->num_keys] = current->pointers[ORDER - 1];
            current->num_keys = mid;
        }

        // 5. 中间关键字上移到父节点
        int middle_key = current->keys[mid];
        if (parent == NULL) {
            // 如果当前节点是根节点,创建新根节点
            Node *new_root = create_node(false);
            new_root->keys[0] = middle_key;
            new_root->pointers[0] = current;
            new_root->pointers[1] = new_node;
            new_root->num_keys = 1;
            tree->root = new_root;
            break;
        } else {
            // 如果有父节点,将中间关键字插入父节点
            int i;
            for (i = parent->num_keys - 1; i >= 0 && parent->keys[i] > middle_key; i--) {
                parent->keys[i + 1] = parent->keys[i];
                parent->pointers[i + 2] = parent->pointers[i + 1];
            }
            parent->keys[i + 1] = middle_key;
            parent->pointers[i + 2] = new_node;
            parent->num_keys++;

            // 如果父节点需要分裂,继续向上处理
            current = parent;
            parent = NULL; // 上升,重新找到新父节点
        }
    }
}


// 查找包含关键字的叶子节点
Node *find_leaf(Node *node, int key) {
    if (node == NULL) return NULL;
    Node *current = node;
    while (!current->is_leaf) {
        int i = 0;
        while (i < current->num_keys && key >= current->keys[i]) {
            i++;
        }
        current = (Node *)current->pointers[i];
    }
    return current;
}

// 删除记录中的特定值
bool delete_record_from_leaf(Node *leaf, int key, int value) {
    int i;
    for (i = 0; i < leaf->num_keys; i++) {
        if (leaf->keys[i] == key) {
            Record *record = (Record *)leaf->pointers[i];
            Record *prev = NULL;
            while (record != NULL) {
                if (record->value == value) {
                    if (prev == NULL) {
                        leaf->pointers[i] = record->next;
                    } else {
                        prev->next = record->next;
                    }
                    free(record);
                    // 如果该关键字没有记录链表了
                    if (leaf->pointers[i] == NULL) {
                        for (int j = i; j < leaf->num_keys - 1; j++) {
                            leaf->keys[j] = leaf->keys[j + 1];
                            leaf->pointers[j] = leaf->pointers[j + 1];
                        }
                        leaf->num_keys--;
                    }
                    return true;
                }
                prev = record;
                record = record->next;
            }
        }
    }
    return false;
}
// 区间查找函数:查找区间 [start, end] 中所有符合条件的记录
void range_query(Node *leaf, int start, int end) {
    // 在叶子节点查找起始值
    while (leaf != NULL) {
        for (int i = 0; i < leaf->num_keys; i++) {
            if (leaf->keys[i] >= start && leaf->keys[i] <= end) {
                Record *record = (Record *)leaf->pointers[i];
                // 输出该键的所有记录
                while (record != NULL) {
                    printf("Key: %d, Value: %d\n", leaf->keys[i], record->value);
                    record = record->next;
                }
            }
        }
        leaf = leaf->next; // 移动到下一个叶子节点
    }
}

// 查找最左侧的叶子节点
Node *find_leftmost_leaf(Node *root) {
    Node *current = root;
    while (current != NULL && !current->is_leaf) {
        current = (Node *)current->pointers[0]; // 一直向左子节点遍历
    }
    return current;
}

// 全纪录检索函数:遍历所有记录
void full_scan(BPlusTree *tree) {
    Node *leaf = find_leftmost_leaf(tree->root); // 从最左侧叶子节点开始
    if (leaf == NULL) {
        printf("Tree is empty.\n");
        return;
    }
    // 遍历所有叶子节点
    while (leaf != NULL) {
        for (int i = 0; i < leaf->num_keys; i++) {
            Record *record = (Record *)leaf->pointers[i];
            // 输出该键的所有记录
            while (record != NULL) {
                printf("Key: %d, Value: %d\n", leaf->keys[i], record->value);
                record = record->next;
            }
        }
        leaf = leaf->next; // 移动到下一个叶子节点
    }
}

int main() {
    BPlusTree *tree = initialize_tree();

    // 插入示例数据
     insert(tree, 10, 100);
    insert(tree, 20, 200);
    insert(tree, 10, 150); // 插入重复关键字
    insert(tree, 30, 300);
    insert(tree, 25, 250);
    // 查找数据
    Node *leaf = find_leaf(tree->root, 15); // 找到包含15的叶子节点
    if (leaf != NULL) {
        printf("Records for key 15:\n");
        Record *record = (Record *)leaf->pointers[0];
        while (record != NULL) {
            printf("%d ", record->value);
            record = record->next;
        }
        printf("\n");
    } else {
        printf("Key 15 not found.\n");
    }
    
    printf("Range Query for keys in [15, 25]:\n");
    
    range_query(leaf, 15, 25);

    // 执行全纪录检索
    printf("\nFull Scan:\n");
    leaf = tree->root; // 从根开始扫描
    full_scan(tree);

   // 删除
    delete_record_from_leaf(leaf, 10, 100);
    printf("Deleted 100 from key 10.\n");

    return 0;
}

博主写了一个静态的插入,如果要写成用户输入的插入,改main函数即可

里面都有注释,不懂的可以仔细想想,把不懂的地方放在评论区,我会尽力给大家解答~

总结

  • 从单点插入可以看到,B+树还有一个小缺点,即如果在非叶子结点找到对应的key,无效(因为不存value),只能继续找到叶子结点进行定位
  • 学习B+树的动态演示网站 B+ Tree Visualization,如果对过程还不是很了解的同学可以自己动手看一看~

B+树的应用场景

在这里插入图片描述
B+树在数据库的检索过程中有很强的应用场景,比如说这个图里,就是一页数据库所存储的,每一页都有一个不同的头指针

但还有个不同的说法,如图所示
在这里插入图片描述

参考文献

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值