有重复值的B+树的插入,删除,单点查找,区间查找,全纪录检索
B+树的由来
B+树是在B树的基础上加以改进的,如果还有对B树不熟悉的同学,可以先看看这篇 B树
B树有两个缺点:
-
区间查找不好实现,如果我要找[31,55]之间的数,他需要把区间查找退化为单点查找,复杂度很高
-
全纪录检索不好实现,输出所有值,也就相当于第一个区间查找[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)
-
如果无法借用关键字,需要将当前节点与其兄弟节点合并。
-
合并操作:
- 将当前节点和兄弟节点的关键字合并到一个节点中。
- 如果是叶子节点,保持
next
指针的连续性。 - 删除父节点中的索引,并更新指针。
-
如果合并后导致父节点关键字数量不足,递归向上调整。
-
举个例子:
这个也是对应一个节点有两个分叉点的,和我们刚刚讲的不太一样
删除的核心:
-
保持平衡:借用和合并确保所有节点的关键字数量不低于 Min_KeysMin_KeysMin_Keys。
-
保持顺序性:合并和调整索引时,关键字顺序保持不变。
-
高度递归调整:删除可能从叶子节点递归调整到根节点。
单点查找
举例
比如说我要找下图中的11
- 查找第一个大于要找的key值,从根节点往下找,先找到22,
- 再进入22所指的下一层,查找第一个大于要找的key值,即13
- 再进入13所指的下一层,找到了等于11的节点,再检查是不是叶子结点(因为非叶子结点只存key,作为查找的标识符,叶子节点才找value),发现是叶子节点
- 然后找到他指向的一个链表所对应的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+树在数据库的检索过程中有很强的应用场景,比如说这个图里,就是一页数据库所存储的,每一页都有一个不同的头指针
但还有个不同的说法,如图所示
参考文献
- 鲁法明,曾庆田,王婷,贾瑞升,包云霞.新工科数据结构.1版.中国矿业大学出版社
- B+树的每个节点最少数量
- B+树的原理及实现
- B+树可视化演示
- MySQL索引底层
- Innodb 中的B+树如何处理重复Key的?