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

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[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 个子节点(那也就是说共 2 m − 1 2m-1 2m1 项内容)。B+ 树有 8 条定义律 :

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

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

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

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

  5. 路由键范围划分:
    –对任意内部节点键 K_i:
    – 子节点 c h i l d r e n [ i ] 中所有键 ≤ K i children[i] 中所有键 ≤ K_i children[i]中所有键Ki
    – 子节点 c h i l d r e n [ i + 1 ] 中所有键 > K i children[i+1] 中所有键 > K_i children[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 = 2 3-1=2 31=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);  //  展开递归,递归的最内层就是叶子节点
    //-------------------------- 回溯 ----------------------------//
    
    // 子节点已完成更新键值,现在父节点对应的路牌也要更新成对应子节点的最小key
    if (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: 1500

Deleting 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$ 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值