当前位置: 首页 > ds >正文

【数据结构】B+ 树——高度近似于菌丝网络——详细解说与其 C 代码实现

文章目录

  • B+ 树的定义
  • B+ 树组织数据的方法
    • 往 B+ 树中插入键值对数据
    • 从 B+ 树中删除键值对
  • 把 B+ 树看作是 “真菌网络”——我理解并记忆 B+ 树的方法
  • B+ 树的 C 代码实现
    • 初始化节点、B+ 树
    • B+ 树节点内的二分查找
    • B+ 树的数据插入操作
    • B+ 树的删除数据操作
    • 范围查询与全局遍历
    • 销毁 B+ 树
    • 测试代码(主函数)

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。

B+ 树的定义

B+ 树与 B树 是有一定的相似性,大家可以参考 这篇文章。

B+ 树是一个针对单个有序的索引指标的数据列(键值对型的数据列 )的高效查询算法,它提供了一个组织数据的方法,按照这个方法先行组织数据,而后能高效率的对某个索引键的数据进行查询,而后实现快速的范围查询。

对比发现,红黑树是无法实现范围查询,即对某一个范围内的键值进行数据查询。SPLAY 树和 B 树都能实现范围查询。这些 “树算法” 的搜索方法原理都是一样的,通过让查询键和节点的各个键比大小,从而确定查询的路径。不同于前面所例举的 “树”,B+ 树是需要一直查到叶结点处,才能获取具体数据。

🌲 B+树 与 红黑树 的核心差别

特性红黑树 (Red-Black Tree)B+ 树 (B+ Tree)本质差异
类型二叉平衡搜索树 (每个节点最多2个子节点)多路平衡搜索树 (每个节点有多个子节点,m阶)节点分支数:二叉 vs 多路
平衡性通过颜色和旋转规则保持 大致平衡 (非绝对完美)通过节点分裂/合并保持 绝对平衡 (所有叶子同层)平衡严格性:红黑树高度上限 2log₂(n+1), B+树更矮胖 (logₘn, m>>2)
节点内容每个节点存储:1. 键;2. 数据指针;3. 左右子节点指针;4. 颜色标记内部节点:只存 键 + 子节点指针。//////// 叶子节点:存 键 + 数据指针 + 指向下一个叶子的指针数据位置:红黑树数据分散在所有节点;B+树数据仅存于叶子,内部节点纯路由
范围查询效率中序遍历 (O(n),但需遍历整棵树,内存跳跃访问,缓存不友好)叶子节点链表顺序遍历 (O(n),但连续内存访问,缓存极友好)范围查询性能:B+树碾压式优势 (链表连续访问 vs 树的递归遍历)
树高相对 较高 (O(log₂n))相对 极矮 (O(logₘn), m 通常数百)访问局部性:B+树单节点载入大量键,减少I/O (对磁盘关键)
主要设计目标内存内高效操作 (插入/删除/查找 O(log₂n))减少磁盘I/O次数 (树矮,节点匹配磁盘块大小)优化方向:红黑树优化CPU计算;B+树优化磁盘I/O
适用存储内存 (RAM)磁盘/SSD (辅存)主战场不同:内存 vs 外存

我们常说的 B+ 树,通常都是说 “ m 阶的 B+ 树 ” ,即每一个节点都不能有超过 m-1 个键,不超过 m 个子节点(那也就是说共 2m−12m-12m1 项内容)。B+ 树有 8 条定义律 :

  1. 内部节点:
    – 存储 k 个路由键(⌈m/2⌉−1≤k≤m−1⌈m/2⌉-1 ≤ k ≤ m-1m/21km1
    –包含 k+1 个子节点指针
    –不存储任何数据指针

  2. 叶子节点:
    –存储 k 个键值对(⌈m/2⌉−1≤k≤m−1⌈m/2⌉-1 ≤ k ≤ m-1m/21km1
    –包含 k 个数据指针
    –包含指向下一个叶子节点的指针

  3. 所有叶子节点必须位于同一深度
    –从根到任意叶子的路径长度相等
    –树完全平衡

  4. 键值继承原则:
    –内部节点的键 Ki=其对应子树的最小键K_i = 其对应子树的最小键Ki=其对应子树的最小键
    –即:Ki=min⁡(children[i+1])K_i = \min(children[i+1])Ki=min(children[i+1])

  5. 路由键范围划分:
    –对任意内部节点键 K_i:
    – 子节点 children[i]中所有键≤Kichildren[i] 中所有键 ≤ K_ichildren[i]中所有键Ki
    – 子节点 children[i+1]中所有键>Kichildren[i+1] 中所有键 > K_ichildren[i+1]中所有键>Ki

  6. 数据位置限定:
    – 实际数据只存储在叶子节点
    – 内部节点仅包含路由信息
    – 叶子节点包含完整键集合

  7. 叶子节点必须按键顺序组成单向链表
    – 通过 next 指针连接
    – 链表按键升序排列

  8. 当根节点作为中间节点而存在时,它是一个特殊的中间节点(构成性的例外),它可以仅拥有一个键,两个子节点。这样能为算法在不断插入新节点的时候,节点不断地在分裂和上溢时,更新 B+ 树的同时,作定义上的兜底。

节点类型键数量范围指针数量存储内容
根节点1 ≤ k ≤ m-1k+1路由键+子节点指针
内部节点⌈m/2⌉-1 ≤ k ≤ m-1k+1路由键+子节点指针
叶子节点⌈m/2⌉-1 ≤ k ≤ m-1k键+数据指针

根据以上定义,我们以 4 阶 B+ 树为例,具体解释这些定义条的作用。 每个节点至多拥有 3−1=23-1=231=2 个键,至多 3 个指针。我们可以想象它的中间节点是长这样的,2 个键,键是某个具体数据块的索引 key,key 是可以用来与查询键比大小而后查询出数据的具体位置,从哪条通路走下去;3 个路由指针,每个路由指针指向字节点的地址,这就是导引查询数据的通路。

------------------------------------┐
│ 指针0 │ 键K₁ │ 指针1 │ 键K₂ │ 指针2   │
└------------------------------------

叶子节点是长这样的,他和中间节点一样,共有 3 个指针,3 个键。键是具体数据块本身的索引 key,其中的 2 个指针是具体数据块的指针,1 个指针是指向下一个叶子节点的指针。

-----------------------------------------┐
│ 键K₁:数据指针 │ 键K₂:数据指针 │ next指针    │
└-----------------------------------------

3 阶 B+ 树的叶子节点和中间节点的关系则如下所示

_____________中间节点_________________┌------------------------------------┐│ 指针0 │ 键K3 │ 指针1 │ 键K5 │ 指针2 │└------------------------------------/             |                 |______________________________________________/              |                 |_______________|                                                _____________|                                 |\|/                                              /                                               |_|_                                           _\|/_                                            _\|/_
┌-----------------------------------------┐       ┌-----------------------------------------┐       ┌-----------------------------------------┐
│ 键K1:数据指针 │ 键K2:数据指针 │ 叶子2 的指针 │       │ 键K3:数据指针 │ 键K4:数据指针 │ 叶子3 的指针 │       │ 键K5:数据指针 │ 键K6:数据指针 │ 叶子4 的指针 │
└-----------------------------------------┘       └-----------------------------------------┘       └-----------------------------------------┘
______________叶子节点 1__________________        _______________ 叶子节点 2__________________       ________________叶子节点 3__________________

B+ 树组织数据的方法

只有合理的组织数据,才会有高效率的查询。所谓的 ‘’组织数据“ ,就是插入键值对数据(注意,这不是插入节点!原因是一个节点有可能会有好几份数据。)以及删除键值对数据。

下面我将以 4 阶的 B+ 树为例,去介绍其数据插入和数据删除。

叶子节点的形式约定如下。 A 代表 32 键的 value,B 代表 46 键的 value,“–>” 代表指向下一个叶子节点的指针。

+-+--+-+--+---+
|A|32|B|46|-->|
+-+--+-+--+---+

中间节点、根节点的形式约定是,空白间隔条就是 “路由指针”,走向下一层的节点。

++--++--++
||32||46||
++--++--++

往 B+ 树中插入键值对数据

是插入键值对数据,而非插入节点!原因是一个节点有可能会有好几份数据。

情况一:根节点满员导致的节点分裂,键信息上溢(注意数据 value 并没上传)。至于叶子节点的分裂也是类似的。我们需要注意到的是不同层的中间节点的键是可以重复的。

增加键 50 与 D 值 
+-+--+-+--+-+--+---+
|A|32|B|46|C|47|-->|
+-+--+-+--+-+--+---+节点满员了,需要键的上溢,与节点分裂+-+--+-+--+-+--+-+--+---+|A|32|B|46|C|47|D|50|-->|+-+--+-+--+-+--+-+--+---+++--++||47||++--++/     \/       \/           \
+-+--+-+--+---+         +-+--+-+--+---+
|A|32|B|46|-->|         |C|47|D|50|-->|
+-+--+-+--+---+         +-+--+-+--+---+

情况二:已满员的中间节点插入数据,导致节点分裂,我们需要注意到的是不同层的中间节点的键是可以重复的。

插入一个 45 的新键++--++--++||33||49||++--++--++/   |    \__/    |     \__/		|         \/         |          \
++--++--++  ++--++--++--++  ++--++--++
||25||30||  ||33||43||44||  ||49||50||
++--++--++  ++--++--++--++  ++--++--++
发现节点数据超员了,进行节点分裂++--++--++||32||46||++--++--++/   |    \__/    |     \_________/		|               \/         |                \
++--++--++  ++--++--++--++--++  ++--++--++
||25||30||  ||33||43||44||45||  ||49||50||
++--++--++  ++--++--++--++--++  ++--++--++节点数据平分两份,中间的 44 键上传++--++--++--++||32||44||46||++--++--++--++/   |    |	\__/    |    | 	 \_________/		|     \             \/         |       \            \
++--++--++  ++--++--++  ++--++--++  ++--++--++
||25||30||  ||33||43||  ||44||45||  ||49||50||
++--++--++  ++--++--++  ++--++--++  ++--++--++

情况三:中间节点的节点正常插入和叶子节点的正常插入,就不展示了。

从 B+ 树中删除键值对

是删除键值对数据,而非删除节点!原因是一个节点有可能会有好几份数据。

情况一:叶子节点的删除数据,发现删除之后低于半数,向兄弟节点借数据。(中间节点的数据删除也是类似的)。会连带的把上面带有 33 的键的节点进行数据删除(递归式的删除)。

删除数据 33 ++--++--++||33||47||++--++--++/   |    \__/     \     \____/		 |           \/           \            \
+-+--+-+--+---+    +-+--+---+  +-+--+-+--+---+
|A|25|B|30|-->|    |C|33|-->|  |D|47|E|50|-->|
+-+--+-+--+---+    +-+--+---+  +-+--+-+--+---+向兄弟节点借数据,先抵押给父节点++--++--++||32||47||++--++--++/   |    \__/     \     \____/		 |           \/           \            \
+-+--+-+--+---+                +-+--+-+--+---+
|A|25|B|30|-->|                |D|47|E|50|-->|
+-+--+-+--+---+                +-+--+-+--+---+父节点换数据,再进行数据下渗++--++--++||30||47||++--++--++/   |    \__/     \     \____/		 |           \/           \            \
+-+--+---+    +-+--+---+  +-+--+-+--+---+
|A|25|-->|    |B|30|-->|  |D|47|E|50|-->|
+-+--+---+    +-+--+---+  +-+--+-+--+---+

情况二:叶子节点的删除数据,发现删除之后低于半数,而且兄弟节点数据刚好在半数,也无法借出数据。(中间节点的数据删除也是类似的)。会连带的把上面带有 33 的键的节点进行数据删除(递归式的删除)。故而只好合并节点。

删除键为 30 的节点++--++--++||30||47||++--++--++/   |    \__/     \     \____/		 |           \/           \            \
+-+--+---+    +-+--+---+  +-+--+---+
|A|25|-->|    |B|30|-->|  |D|47|-->|
+-+--+---+    +-+--+---+  +-+--+---+
节点合并++--++--++||30||47||++--++--++/   |    \__/     \     \____/		 |           \/           \            \
+-+--+---+                  +-+--+---+
|A|25|-->|                  |D|47|-->|
+-+--+---+                  +-+--+---+++--++||47||++--++/    \__/      \____/	            \/                  \
+-+--+---+               +-+--+---+
|A|25|-->|               |D|47|-->|
+-+--+---+               +-+--+---+

情况三:正常的中间节点删除、叶子节点删除数据,就不罗列了。

把 B+ 树看作是 “真菌网络”——我理解并记忆 B+ 树的方法

在这里插入图片描述

🍄 B+树的真菌生长比喻
想象一片数字森林,其中生长着一种名为 B+ 菌的神奇真菌:

  1. 孢子萌发(初始状态)
    1、森林中诞生一个初始孢子细胞(根节点兼叶子节点)
    2、这个细胞通过吸收数据养分(插入键值对)逐渐长大

  2. 质配阶段(节点填充)
    1、细胞通过菌丝网络(指针)与其他细胞交换遗传物质(键值传递)
    2、细胞膜(节点容量)不断扩张,容纳更多染色体片段(键值对)
    3、当细胞达到临界质量(阶数M)时,进入减数分裂准备期

  3. 减数分裂(节点分裂)
    1、细胞核发生均等裂变(键值均匀分裂)
    2、产生两个子细胞(新叶子节点)
    3、关键染色体(最小键K3)通过菌丝管道提升到上层细胞(父节点)

  4. 菌丝网络形成(叶子链表)
    1、子细胞间分泌信息素导管(next指针)
    2、形成地下菌丝网络(叶子节点链表)
    3、养分(数据)可通过网络连续输送(范围查询)

  5. 子实体发育(树结构生长)
    1、当上层细胞也达到临界质量时,触发级联分裂
    2、最终在森林地表形成:
    3、菌盖(根节点):指挥中心
    4、菌褶(内部节点):传输通道
    5、菌丝体(叶子链表):营养交换网络

B+ 树的 C 代码实现

我们需要明确的一点是
数据结构=数据定义+数据的操作方法。数据结构 = 数据定义 + 数据的操作方法。数据结构=数据定义+数据的操作方法。

首先是数据定义。

// B+树阶数 - 每个节点最多M-1个键,M个孩子
#define M 4
#define MIN_KEYS ((M) / 2)// B+树节点结构
typedef struct BPlusTreeNode {bool is_leaf;           // 是否为叶子节点int key_count;          // 当前键的数量int keys[M];            // 键数组union {struct BPlusTreeNode* children[M + 1]; // 内部节点的子节点指针void* data_ptrs[M]; // 叶子节点的数据指针};struct BPlusTreeNode* next; // 叶子节点的链表指针
} BPlusTreeNode;// B+树结构
typedef struct {BPlusTreeNode* root;
} BPlusTree;

B+ 树的数据结构

  • 叶子节点 leaf 和中间节点 internal 还是是有区分的。在范围查询上,叶子节点的链表指针提供了便利;叶子节点负责储存数据;
  • 理论上,叶子节点和中间节点都有 M+1 (阶)个指针,只是叶子节点有 M 个数据指针和 1 个叶子链表指针,而中间节点有 M+1 个路由指针。
  • 但实际上,路由指针数组的指针和数据指针被统一在联合体中,降低了节点的内存,叶子和中间点都可以共用同一个数据结构。
  • key_count、0 和 MIN_KEYS 是三个重要的数字,在节点合并、节点分裂、借兄弟数据、叶子删数据和叶子加数据都有涉及。
  • 所有节点都有最多有 M 个键,它们是 M+1 阶的。第 K 个键大于第 K 个路由指针下的任何一个键,第 K 个键小于等于第 K+1 个路由指针下的任何一个键。父节点的第 K 个键等于第 K+1 个子节点的第 0 个键

初始化节点、B+ 树

// 创建新节点
BPlusTreeNode* create_node(bool is_leaf) {BPlusTreeNode* node = (BPlusTreeNode*)malloc(sizeof(BPlusTreeNode));node->is_leaf = is_leaf;node->key_count = 0;//  务必要注意,节点的所有 key 一定要初始化成一个不会与所插入键重复的值,否则很麻烦。node->next = NULL;for (int i = 0; i < M; i++) {node->keys[i] = INT_MAX; // 初始化为最大值}return node;
}// 初始化B+树
BPlusTree* init_bplus_tree() {BPlusTree* tree = (BPlusTree*)malloc(sizeof(BPlusTree));tree->root = create_node(true); // 初始为叶子节点return tree;
}

B+ 树节点内的二分查找

// 在节点中查找键的位置,或者找到合适往下走的通路
int btree_bin_search(BPlusTreeNode *node, int low, int high, int key) {//	二分查找法的时间复杂度是 O(log N)int mid;if (low > high || low < 0 || high < 0) {return -1;}while (low < high) {mid = (low + high) / 2;		//	向 0 取整吗if (key > node->keys[mid]) {low = mid + 1;} else {high = mid - 1;}}return low;
}

B+ 树的数据插入操作

insert_recursive 的递归实现思想不是 B 树的 “防患于未然的未雨绸缪”,而是把 “底层的暗流涌动逐层的浮于水面之上”

  • 叶子节点的数据插入是递归的边界条件
  • 只有叶子节点的插入所导致的节点分裂才有可能导致中间节点键的插入,因而需要逐层递归
  • 回溯性地产生兄弟节点、以及需要提升的键
  • 只要有一层说不用分裂节点,那之后的递归的回溯都会 trivival 化

bplus_tree_insert 函数是从 root 节点往下探索的,最终会包含对根节点的重塑


// 插入键到叶子节点
void insert_into_leaf(BPlusTreeNode* leaf, int key, void* value) {int idx =btree_bin_search(leaf, 0, leaf->key_count, key);// 如果键已存在,更新值if (idx < leaf->key_count && leaf->keys[idx] == key) {leaf->data_ptrs[idx] = value;return;}// 移动键和值以腾出空间for (int i = leaf->key_count; i > idx; i--) {leaf->keys[i] = leaf->keys[i - 1];leaf->data_ptrs[i] = leaf->data_ptrs[i - 1];}// 插入新键值leaf->keys[idx] = key;leaf->data_ptrs[idx] = value;leaf->key_count++;
}// 分裂叶子节点
BPlusTreeNode* split_leaf_node(BPlusTreeNode* leaf) {//  BPlusTreeNode* new_leaf = create_node(true);    //  true 代表生产叶子节点int split_index = leaf->key_count / 2;          //  中间指标是下一个叶子节点的键的开始指标// 移动后半部分键值到新叶子节点for (int i = split_index; i < leaf->key_count; i++) {new_leaf->keys[i - split_index] = leaf->keys[i];new_leaf->data_ptrs[i - split_index] = leaf->data_ptrs[i];leaf->keys[i] = INT_MAX; // 重置原位置}new_leaf->key_count = leaf->key_count - split_index;leaf->key_count = split_index;// 维护叶子节点链表new_leaf->next = leaf->next;leaf->next = new_leaf;return new_leaf;
}// 分裂内部节点,这是从终端由下往上传来的压力
BPlusTreeNode* split_internal_node(BPlusTreeNode* internal) {BPlusTreeNode* new_internal = create_node(false);int split_index = internal->key_count / 2;      //  分裂的位置int promote_key = internal->keys[split_index];  //  需要提升的键// 移动键和子节点指针,分裂出的右兄弟节点,不包含需要提升for (int i = split_index + 1; i < internal->key_count; i++) {new_internal->keys[i - split_index - 1] = internal->keys[i];new_internal->children[i - split_index - 1] = internal->children[i];internal->keys[i] = INT_MAX;}// 处理最后一个子节点指针new_internal->children[internal->key_count - split_index - 1] = internal->children[internal->key_count];new_internal->key_count = internal->key_count - split_index - 1;internal->key_count = split_index;return new_internal;
}// 插入操作的递归实现
void insert_recursive(BPlusTreeNode* node, int key, void* value,  BPlusTreeNode** new_child, int* promote_key) {//  node 和 new_child 是同级别的,node 分裂之后会产生右兄弟节点,去到更上一层后还会继续被更新的//  如果是叶子节点if (node->is_leaf) {insert_into_leaf(node, key, value); //  叶子先插入数据// 检查是否需要分裂,如果现在满员了,我们可以未雨绸缪先行分裂if (node->key_count == M) {BPlusTreeNode* new_node = split_leaf_node(node);*new_child = new_node;*promote_key = new_node->keys[0]; // 提升新节点的第一个键,叶子节点允许与中间节点用同样的键} else {*new_child = NULL;}return;     //  叶子节点是不会执行下面这些代码的,是递归函数的边界条件}// 内部节点 - 找到合适的子节点int idx =btree_bin_search(node, 0, node->key_count, key);//  找到分裂的位置BPlusTreeNode* child = node->children[idx];     BPlusTreeNode* new_child_node = NULL;int new_key = INT_MAX;insert_recursive(child, key, value, &new_child_node, &new_key); // 递归插入子节点,递归展开//------------------- 回溯 --------------------////  new_child_node 和 new_key 会被回溯定义,直至浮于水面,层层向上反映//  如果子节点分裂了,需要判断当前更高一层的节点是否需要分裂if (new_child_node) {//  在内部节点中为新键腾出空间for (int i = node->key_count; i > idx; i--) {node->keys[i] = node->keys[i - 1];node->children[i + 1] = node->children[i];}// 插入新键和子节点指针node->keys[idx] = new_key;node->children[idx + 1] = new_child_node;node->key_count++;// 检查是否需要分裂当前节点if (node->key_count == M) {BPlusTreeNode* new_node = split_internal_node(node);//  反射回草稿纸,记录新产生的节点和需要提升的键*new_child = new_node;  *promote_key = node->keys[node->key_count]; // 提升新分裂节点的首键} else {*new_child = NULL;  //  本层节点不需要分裂}} else {*new_child = NULL;  //  子节点没有分裂,那父节点更没有分裂}
}// B+树插入操作
void bplus_tree_insert(BPlusTree* tree, int key, void* value) {//  以下两个变量取什么值并不重要,它们本身只是用来迭代求解的一张草稿纸BPlusTreeNode* new_child = NULL;    //  或许会分裂出的新节点int promote_key = INT_MAX;          //  或许有需要提升的键//  由于这两个变量会经历递归求解,最终有可能反射为,当前根节点的兄弟节点insert_recursive(tree->root, key, value, &new_child, &promote_key); //  递归展开//  底层插入数据的行为会一层一层地反映到根结点处//  root 和 new_child 是同级别的// 处理根节点分裂,产生一个新的根节点if (new_child) {BPlusTreeNode* new_root = create_node(false);   //  中间节点new_root->keys[0] = promote_key;new_root->children[0] = tree->root;new_root->children[1] = new_child;new_root->key_count = 1;tree->root = new_root;}
}

B+ 树的删除数据操作

delete_recursive 的递归实现思想依然是把 “底层的暗流涌动逐层的浮于水面之上”

  • 只有叶子的删除有可能导致节点合并,节点合并会导致中间节点的键删除,这又有可能导致节点的再次合并。
  • 叶子节点的数据删除是递归的边界条件
  • 回溯性的判断本层次是否需要节点合并、借兄弟节点数据,如果是后者或者都没必要,此后的递归回溯都会 trivival 化

// 在叶子节点处删除键值对数据
void delete_from_leaf(BPlusTreeNode* leaf, int key) {int idx =btree_bin_search(leaf, 0, leaf->key_count, key);if (idx < leaf->key_count && leaf->keys[idx] == key) {// 移动键和值以填充空隙for (int i = idx; i < leaf->key_count - 1; i++) {leaf->keys[i] = leaf->keys[i + 1];leaf->data_ptrs[i] = leaf->data_ptrs[i + 1];}leaf->keys[leaf->key_count - 1] = INT_MAX;leaf->key_count--;}
}// 从兄弟节点借键(叶子节点)
void borrow_from_sibling(BPlusTreeNode* node, BPlusTreeNode* sibling, BPlusTreeNode* parent, int parent_key_index, bool is_left_sibling) {//  sibling 指代兄弟节点if (is_left_sibling) {// 从左侧兄弟借键,只能借最大键和其对应的数据(或者路标指针)int last_key = sibling->keys[sibling->key_count - 1];if (node->is_leaf) {//  叶子节点的兄弟借数据void* last_value = sibling->data_ptrs[sibling->key_count - 1];// 移动当前节点的键值,挪位置for (int i = node->key_count; i > 0; i--) {node->keys[i] = node->keys[i - 1];node->data_ptrs[i] = node->data_ptrs[i - 1];}         // 插入借来的键值node->keys[0] = last_key;node->data_ptrs[0] = last_value;node->key_count++;       } else {//  中间节点的兄弟借数据BPlusTreeNode* last_child = sibling->children[sibling->key_count];// 移动当前节点的键值,挪位置for (int i = node->key_count; i > 0; i--) {node->keys[i] = node->keys[i - 1];node->children[i] = node->data_ptrs[i - 1];}// 插入借来的键值node->keys[0] = last_key;node->children[0] = last_child;node->key_count++;}// 更新兄弟节点sibling->keys[sibling->key_count - 1] = INT_MAX;sibling->key_count--;// 更新父节点键parent->keys[parent_key_index-1] = node->keys[0];} else {// 从右侧兄弟借键,只能借最小键和其对应的数据int first_key = sibling->keys[0];if (node->is_leaf) {//  叶子节点的兄弟借数据void* first_value = sibling->data_ptrs[0];// 插入借来的键值node->keys[node->key_count] = first_key;node->data_ptrs[node->key_count] = first_value;node->key_count++;// 更新兄弟节点for (int i = 0; i < sibling->key_count - 1; i++) {sibling->keys[i] = sibling->keys[i + 1];sibling->data_ptrs[i] = sibling->data_ptrs[i + 1];}} else {//  中间节点的兄弟借数据BPlusTreeNode* first_child = sibling->children[0];// 插入借来的键值node->keys[node->key_count] = first_key;node->children[node->key_count] = first_child;node->key_count++;// 更新兄弟节点for (int i = 0; i < sibling->key_count - 1; i++) {sibling->keys[i] = sibling->keys[i + 1];sibling->children[i] = sibling->children[i + 1];}}sibling->keys[sibling->key_count - 1] = INT_MAX;sibling->key_count--;// 更新父节点键node->keys[parent_key_index] = sibling->keys[0];}
}// 合并节点
void merge_nodes(BPlusTreeNode* left, BPlusTreeNode* right) {//  节点一旦合并,就意味着兄弟节点都没有余粮可借了// 移动右侧节点的键值到左侧if (left->is_leaf) {for (int i = 0; i < right->key_count; i++) {left->keys[left->key_count + i] = right->keys[i];left->data_ptrs[left->key_count + i] = right->data_ptrs[i];}left->key_count += right->key_count;left->next = right->next;// 释放右侧节点free(right);} else {for (int i = 0; i < right->key_count; i++) {left->keys[left->key_count + i] = right->keys[i];left->children[left->key_count + i] = right->data_ptrs[i];}left->children[left->key_count + right->key_count] = right->children[right->key_count];left->key_count += right->key_count;// 释放右侧节点free(right);}}// 删除操作的递归实现
bool delete_recursive(BPlusTreeNode* node, int key, int* min_key) {//  min_key 指代 node 更新后的最小键值,它会被递归更新的,直至浮出水面//  返回值 bool 表示操作是否需要进一步回溯修改。在叶子节点层次,删除 node 的一个数据后,数据的容量是否低于最小限度。 在中间节点层次,适应底层修改后,数据的容量是否低于最小限度。//  节点合并有可能会导致问题上传,借节点则不会导致问题上传// 叶子节点处理if (node->is_leaf) {//  这是递归的边界条件int idx =btree_bin_search(node, 0, node->key_count, key);if (idx < node->key_count && node->keys[idx] == key) {delete_from_leaf(node, key);*min_key = (node->key_count > 0) ? node->keys[0] : INT_MAX;return node->key_count < MIN_KEYS;  //  节点数据量严重缺乏}return false; // 键不存在}// 内部节点 - 找到合适的子节点int idx = btree_bin_search(node, 0, node->key_count, key);    //  idx 相位所对应的路由指针上所有的键都小于本节点第 idx 位的键BPlusTreeNode* child = node->children[idx]; //  找到路牌往下走int new_min_key;bool child_underflow = delete_recursive(child, key, &new_min_key);  //  展开递归,递归的最内层就是叶子节点//-------------------------- 回溯 ----------------------------//// 子节点已完成更新键值,现在父节点对应的路牌也要更新成对应子节点的最小keyif (idx > 0 && new_min_key != INT_MAX && new_min_key != node->keys[idx - 1]) {node->keys[idx - 1] = new_min_key;}// 处理子节点下溢if (child_underflow) {BPlusTreeNode* left_sibling = (idx > 0) ? node->children[idx - 1] : NULL;BPlusTreeNode* right_sibling = (idx < node->key_count) ? node->children[idx + 1] : NULL;// 尝试从左兄弟借键if (left_sibling && left_sibling->key_count > MIN_KEYS) {//  借数据并不会导致上层结构的变化borrow_from_sibling(child, left_sibling, node, idx, true);return false;} // 尝试从右兄弟借键else if (right_sibling && right_sibling->key_count > MIN_KEYS) {//  借数据并不会导致上层结构的变化borrow_from_sibling(child, right_sibling, node, idx, false);return false;}// 合并节点else {if (left_sibling) {// 与左兄弟合并if (child->is_leaf) {merge_nodes(left_sibling, child);}// 更新父节点for (int i = idx; i < node->key_count; i++) {node->keys[i - 1] = node->keys[i];node->children[i] = node->children[i + 1];}node->keys[node->key_count - 1] = INT_MAX;node->key_count--;} else if (right_sibling) {// 与右兄弟合并if (child->is_leaf) {merge_nodes(child, right_sibling);}// 更新父节点for (int i = idx + 1; i < node->key_count; i++) {node->keys[i - 1] = node->keys[i];node->children[i] = node->children[i + 1];}node->keys[node->key_count - 1] = INT_MAX;node->key_count--;}return node->key_count < MIN_KEYS;}}return false;
}// B+树删除操作
void bplus_tree_delete(BPlusTree* tree, int key) {int min_key;bool root_underflow = delete_recursive(tree->root, key, &min_key);  //  让所有问题浮出水面// 处理根节点下溢if (root_underflow && !tree->root->is_leaf && tree->root->key_count == 0) {BPlusTreeNode* old_root = tree->root;tree->root = tree->root->children[0];free(old_root);}
}

范围查询与全局遍历

B+ 树的范围查询要远比 B 树的好写,因为 B+ 树的叶子节点是可以指向下一个相邻的叶子节点。

// 查找键对应的值
void* bplus_tree_search(BPlusTree* tree, int key) {BPlusTreeNode* node = tree->root;while (!node->is_leaf) {int idx =btree_bin_search(node, 0, node->key_count, key);node = node->children[idx];}int idx =btree_bin_search(node, 0, node->key_count, key);if (idx < node->key_count && node->keys[idx] == key) {return node->data_ptrs[idx];}return NULL; // 未找到
}// 范围查询
void bplus_tree_range_query(BPlusTree* tree, int start, int end) {BPlusTreeNode* node = tree->root;// 找到起始键所在的叶子节点while (!node->is_leaf) {int idx =btree_bin_search(node, 0, node->key_count, start);node = node->children[idx];}// 在叶子节点链表中遍历while (node != NULL) {for (int i = 0; i < node->key_count; i++) {// 键在范围内if (node->keys[i] >= start && node->keys[i] <= end) {printf("Key: %d, Value: %d\n", node->keys[i], *(int*)(node->data_ptrs[i]));  //  %p 是指针占位符}// 超过范围,停止查询if (node->keys[i] > end) {return;}}node = node->next;}
}

我们亦可按照树的层次去打印 B+ 树的结构

// 打印B+树(辅助函数)
void print_bplus_tree(BPlusTreeNode* node, int level) {if (node == NULL) return;printf("Level %d: ", level);for (int i = 0; i < node->key_count; i++) {printf("%d ", node->keys[i]);}printf("\n");if (!node->is_leaf) {for (int i = 0; i <= node->key_count; i++) {print_bplus_tree(node->children[i], level + 1);}}
}

销毁 B+ 树

// 释放B+树内存
void free_bplus_tree(BPlusTreeNode* node) {if (node == NULL) return;if (!node->is_leaf) {for (int i = 0; i <= node->key_count; i++) {free_bplus_tree(node->children[i]);}}free(node);
}

测试代码(主函数)

测试量为 200 个插入。

// 测试用例
int main() {BPlusTree* tree = init_bplus_tree();// 插入测试printf("Inserting keys...\n");for (int i = 1; i <= 200; i++) {int* value = (int*)malloc(sizeof(int));*value = i * 100;bplus_tree_insert(tree, i, value);}printf("B+ Tree structure:\n");print_bplus_tree(tree->root, 0);// 搜索测试printf("\nSearching keys:\n");for (int i = 5; i <= 15; i += 3) {void* result = bplus_tree_search(tree, i);if (result) {printf("Key %d found. Value: %d\n", i, *((int*)result));} else {printf("Key %d not found.\n", i);}}// 范围查询测试printf("\nRange query [7, 15]:\n");bplus_tree_range_query(tree, 7, 15);// 删除测试printf("\nDeleting keys 8, 12, 15...\n");bplus_tree_delete(tree, 8);bplus_tree_delete(tree, 12);bplus_tree_delete(tree, 15);printf("\nB+ Tree structure after deletion:\n");print_bplus_tree(tree->root, 0);// 再次范围查询printf("\nRange query [7, 15] after deletion:\n");bplus_tree_range_query(tree, 7, 15);// 清理free_bplus_tree(tree->root);free(tree);return 0;
}

运行效果还 OK。

qiming@qiming:~/share/CTASK/data-structure$ gcc -o bp bptree_test.c
qiming@qiming:~/share/CTASK/data-structure$ ./bp
Inserting keys...
B+ Tree structure:
Level 0: 55 109 163 
Level 1: 19 37 
Level 2: 7 13 
Level 3: 3 5 
Level 4: 1 2 
Level 4: 3 4 
Level 4: 5 6 
Level 3: 9 11 
Level 4: 7 8 
Level 4: 9 10 
Level 4: 11 12 
Level 3: 15 17 
Level 4: 13 14 
Level 4: 15 16 
Level 4: 17 18 
Level 2: 25 31 
Level 3: 21 23 
Level 4: 19 20 
Level 4: 21 22 
Level 4: 23 24 
Level 3: 27 29 
Level 4: 25 26 
Level 4: 27 28 
Level 4: 29 30 
Level 3: 33 35 
Level 4: 31 32 
Level 4: 33 34 
Level 4: 35 36 
Level 2: 43 49 
Level 3: 39 41 
Level 4: 37 38 
Level 4: 39 40 
Level 4: 41 42 
Level 3: 45 47 
Level 4: 43 44 
Level 4: 45 46 
Level 4: 47 48 
Level 3: 51 53 
Level 4: 49 50 
Level 4: 51 52 
Level 4: 53 54 
Level 1: 73 91 
Level 2: 61 67 
Level 3: 57 59 
Level 4: 55 56 
Level 4: 57 58 
Level 4: 59 60 
Level 3: 63 65 
Level 4: 61 62 
Level 4: 63 64 
Level 4: 65 66 
Level 3: 69 71 
Level 4: 67 68 
Level 4: 69 70 
Level 4: 71 72 
Level 2: 79 85 
Level 3: 75 77 
Level 4: 73 74 
Level 4: 75 76 
Level 4: 77 78 
Level 3: 81 83 
Level 4: 79 80 
Level 4: 81 82 
Level 4: 83 84 
Level 3: 87 89 
Level 4: 85 86 
Level 4: 87 88 
Level 4: 89 90 
Level 2: 97 103 
Level 3: 93 95 
Level 4: 91 92 
Level 4: 93 94 
Level 4: 95 96 
Level 3: 99 101 
Level 4: 97 98 
Level 4: 99 100 
Level 4: 101 102 
Level 3: 105 107 
Level 4: 103 104 
Level 4: 105 106 
Level 4: 107 108 
Level 1: 127 145 
Level 2: 115 121 
Level 3: 111 113 
Level 4: 109 110 
Level 4: 111 112 
Level 4: 113 114 
Level 3: 117 119 
Level 4: 115 116 
Level 4: 117 118 
Level 4: 119 120 
Level 3: 123 125 
Level 4: 121 122 
Level 4: 123 124 
Level 4: 125 126 
Level 2: 133 139 
Level 3: 129 131 
Level 4: 127 128 
Level 4: 129 130 
Level 4: 131 132 
Level 3: 135 137 
Level 4: 133 134 
Level 4: 135 136 
Level 4: 137 138 
Level 3: 141 143 
Level 4: 139 140 
Level 4: 141 142 
Level 4: 143 144 
Level 2: 151 157 
Level 3: 147 149 
Level 4: 145 146 
Level 4: 147 148 
Level 4: 149 150 
Level 3: 153 155 
Level 4: 151 152 
Level 4: 153 154 
Level 4: 155 156 
Level 3: 159 161 
Level 4: 157 158 
Level 4: 159 160 
Level 4: 161 162 
Level 1: 181 
Level 2: 169 175 
Level 3: 165 167 
Level 4: 163 164 
Level 4: 165 166 
Level 4: 167 168 
Level 3: 171 173 
Level 4: 169 170 
Level 4: 171 172 
Level 4: 173 174 
Level 3: 177 179 
Level 4: 175 176 
Level 4: 177 178 
Level 4: 179 180 
Level 2: 187 193 
Level 3: 183 185 
Level 4: 181 182 
Level 4: 183 184 
Level 4: 185 186 
Level 3: 189 191 
Level 4: 187 188 
Level 4: 189 190 
Level 4: 191 192 
Level 3: 195 197 199 
Level 4: 193 194 
Level 4: 195 196 
Level 4: 197 198 
Level 4: 199 200 Searching keys:
Key 5 not found.
Key 8 not found.
Key 11 not found.
Key 14 not found.Range query [7, 15]:
Key: 7, Value: 700
Key: 8, Value: 800
Key: 9, Value: 900
Key: 10, Value: 1000
Key: 11, Value: 1100
Key: 12, Value: 1200
Key: 13, Value: 1300
Key: 14, Value: 1400
Key: 15, Value: 1500Deleting keys 8, 12, 15...B+ Tree structure after deletion:
Level 0: 55 109 163 
Level 1: 19 37 
Level 2: 7 21997 
Level 3: 3 32545 
Level 4: 1 2 
Level 4: 3 4 
Level 4: 5 6 
Level 3: 9 11 
Level 4: 7 8 
Level 4: 9 10 
Level 4: 11 12 
Level 3: 15 17 
Level 4: 13 14 
Level 4: 15 16 
Level 4: 17 18 
Level 2: 25 31 
Level 3: 21 23 
Level 4: 19 20 
Level 4: 21 22 
Level 4: 23 24 
Level 3: 27 29 
Level 4: 25 26 
Level 4: 27 28 
Level 4: 29 30 
Level 3: 33 35 
Level 4: 31 32 
Level 4: 33 34 
Level 4: 35 36 
Level 2: 43 49 
Level 3: 39 41 
Level 4: 37 38 
Level 4: 39 40 
Level 4: 41 42 
Level 3: 45 47 
Level 4: 43 44 
Level 4: 45 46 
Level 4: 47 48 
Level 3: 51 53 
Level 4: 49 50 
Level 4: 51 52 
Level 4: 53 54 
Level 1: 73 91 
Level 2: 61 67 
Level 3: 57 59 
Level 4: 55 56 
Level 4: 57 58 
Level 4: 59 60 
Level 3: 63 65 
Level 4: 61 62 
Level 4: 63 64 
Level 4: 65 66 
Level 3: 69 71 
Level 4: 67 68 
Level 4: 69 70 
Level 4: 71 72 
Level 2: 79 85 
Level 3: 75 77 
Level 4: 73 74 
Level 4: 75 76 
Level 4: 77 78 
Level 3: 81 83 
Level 4: 79 80 
Level 4: 81 82 
Level 4: 83 84 
Level 3: 87 89 
Level 4: 85 86 
Level 4: 87 88 
Level 4: 89 90 
Level 2: 97 103 
Level 3: 93 95 
Level 4: 91 92 
Level 4: 93 94 
Level 4: 95 96 
Level 3: 99 101 
Level 4: 97 98 
Level 4: 99 100 
Level 4: 101 102 
Level 3: 105 107 
Level 4: 103 104 
Level 4: 105 106 
Level 4: 107 108 
Level 1: 127 145 
Level 2: 115 121 
Level 3: 111 113 
Level 4: 109 110 
Level 4: 111 112 
Level 4: 113 114 
Level 3: 117 119 
Level 4: 115 116 
Level 4: 117 118 
Level 4: 119 120 
Level 3: 123 125 
Level 4: 121 122 
Level 4: 123 124 
Level 4: 125 126 
Level 2: 133 139 
Level 3: 129 131 
Level 4: 127 128 
Level 4: 129 130 
Level 4: 131 132 
Level 3: 135 137 
Level 4: 133 134 
Level 4: 135 136 
Level 4: 137 138 
Level 3: 141 143 
Level 4: 139 140 
Level 4: 141 142 
Level 4: 143 144 
Level 2: 151 157 
Level 3: 147 149 
Level 4: 145 146 
Level 4: 147 148 
Level 4: 149 150 
Level 3: 153 155 
Level 4: 151 152 
Level 4: 153 154 
Level 4: 155 156 
Level 3: 159 161 
Level 4: 157 158 
Level 4: 159 160 
Level 4: 161 162 
Level 1: 181 
Level 2: 169 175 
Level 3: 165 167 
Level 4: 163 164 
Level 4: 165 166 
Level 4: 167 168 
Level 3: 171 173 
Level 4: 169 170 
Level 4: 171 172 
Level 4: 173 174 
Level 3: 177 179 
Level 4: 175 176 
Level 4: 177 178 
Level 4: 179 180 
Level 2: 187 193 
Level 3: 183 185 
Level 4: 181 182 
Level 4: 183 184 
Level 4: 185 186 
Level 3: 189 191 
Level 4: 187 188 
Level 4: 189 190 
Level 4: 191 192 
Level 3: 195 197 199 
Level 4: 193 194 
Level 4: 195 196 
Level 4: 197 198 
Level 4: 199 200 Range query [7, 15] after deletion:
Key: 7, Value: 700
Key: 8, Value: 800
Key: 9, Value: 900
Key: 10, Value: 1000
Key: 11, Value: 1100
Key: 12, Value: 1200
Key: 13, Value: 1300
Key: 14, Value: 1400
Key: 15, Value: 1500
qiming@qiming:~/share/CTASK/data-structure$ 
http://www.xdnf.cn/news/18656.html

相关文章:

  • 面向RF设计人员的微带贴片天线计算器
  • AI领域的语义空间是什么?
  • ES6变量与解构:let、const与模板字符串全解析
  • 「越短越合法」型滑动窗口
  • 解释一下,Linux,shell,Vmware,Ubuntu,以及Linux命令和shell命令的区别
  • 111、【OS】【Nuttx】【周边】效果呈现方案解析:-print0 选项
  • Linux操作系统编程——网络
  • 第二阶段WinFrom-6:文件对话框,对象的本地保存,序列化与反序列化,CSV文件操作,INI文件读写
  • 08.21总结
  • Claude Code 三类.md文件
  • Day2--HOT100--283. 移动零,11. 盛最多水的容器,15. 三数之和
  • PCB电路设计学习2 元件原理图封装的添加 手工设计元件封装
  • 大型前端项目如何实现css 隔离:利用浏览器原生的 Shadow DOM 完全隔离 DOM 结构与样式...
  • Linux 下的网络编程
  • 学习嵌入式的第二十四天——数据结构——队列和树
  • Git 提交除某个文件外的其他所有文件
  • 微信开发者工具:更改 AppID 失败
  • 嵌入式-EXTI的工作原理和按钮实验-Day19
  • 我从零开始学习C语言(13)- 循环语句 PART2
  • QT-窗口类部件
  • K8S高可用集群
  • K8s的相关知识总结
  • 如何理解面向过程和面向对象,举例说明一下?
  • Qt5 的跨平台开发详细讲解
  • 计算机毕设选题推荐 基于Spark的家庭能源消耗智能分析与可视化系统 基于机器学习的家庭能源消耗预测与可视化系统源码
  • 告别第三方流氓工具,如何实现纯净系统维护
  • DIC技术极端环境高温案例分享——从1600℃的锆合金力学性能测试到3000℃变形测试的DIC测量
  • 手机、电脑屏幕的显示坏点检测和成像原理
  • k8s----学习站点搭建
  • C++显示类型转换运算符static_cast使用指南