B+ 树删除实现:从 Bug 到稳定架构的完整复盘
关键词:B+ 树、删除操作、下溢(underflow)、borrow / merge、fixup、架构设计
这篇文章不是一份“标准答案式”的 B+ 树删除教程,而是一份真实的实现复盘:
- 为什么“只删一个 key”,树却会整体崩掉?
- 为什么 merge 看似成功,parent 却被删空?
- 为什么修复逻辑一定要拆成
fix_leaf / fix_internal?
如果你正在手写 B+ 树,尤其是 C/C++ 版本,那么这篇文章基本可以帮你避开 90% 的坑。
一、B+ 树删除:真正的难点在哪?
很多教程会告诉你:
删除 = 从叶子删 → 不够就借 → 借不到就合并 → 向上递归
但真正的难点不在算法步骤,而在“职责边界”。
也就是说:
- 谁负责修改 parent?
- merge 做到哪一步为止?
- underflow 是谁检测、谁修复?
如果这些问题不想清楚,代码一定会出现:
- parent key 被删两次
- root 被误判为空
- children 指针错位
- 树结构“看起来还能跑,但已经坏了”
二、基础定义(以 Order = 6 为例)
1 2 3 4
| #define ORDER 6 #define MAX_KEYS (ORDER - 1) #define MAX_CHILDREN ORDER #define MIN_KEYS ((ORDER + 1) / 2 - 1)
|
- 叶子节点 / 内部节点:最少
MIN_KEYS
- root 允许特例:可以少于 MIN_KEYS
三、总体设计原则(非常重要)
👉 核心原则:单一职责
在最终稳定版本中,我采用的是:
方案 A:merge 只合并,不修改 parent
也就是说:
| 模块 |
只做什么 |
不做什么 |
| delete_from_leaf |
删除 key |
不修结构 |
| fix_leaf |
borrow / merge |
不删 parent key |
| merge_leaf |
合并两个叶子 |
不动 parent |
| delete_from_internal |
删除 parent key |
不合并节点 |
| fix_internal |
修复 internal |
不越权删除 |
📌 所有 parent 的 key / child 删除,只能在一个地方发生。
四、删除总入口:bptree_delete
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int bptree_delete(bptree* tree, int key) { bptree_node* leaf = bptree_find_leaf(tree, key); if (!leaf) return BPTREE_ERR;
if (bptree_delete_from_leaf(leaf, key) != BPTREE_OK) return BPTREE_ERR;
if (leaf == tree->root) return BPTREE_OK;
if (bptree_is_underflow(tree, leaf)) { bptree_fix_leaf(tree, leaf); } return BPTREE_OK; }
|
关键点:
- delete 只负责“触发修复”
- 不关心 borrow / merge 细节
五、下溢判断:bptree_is_underflow
1 2 3 4
| int bptree_is_underflow(bptree* tree, bptree_node* node) { if (node == tree->root) return BPTREE_OK; return node->key_count < MIN_KEYS; }
|
📌 root 是特例,千万别忘。
六、叶子修复:bptree_fix_leaf
修复顺序(必须这样):
- borrow left
- borrow right
- merge
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void bptree_fix_leaf(bptree* tree, bptree_node* leaf) { if (bptree_borrow_from_left_leaf(leaf) == BPTREE_OK) return; if (bptree_borrow_from_right_leaf(leaf) == BPTREE_OK) return;
int parent_key_idx = -1;
bptree_node* left = bptree_get_left_sibling(leaf); bptree_node* right = bptree_get_right_sibling(leaf);
bptree_node* merge_node = NULL;
if (left) { merge_node = left; } else if (right) { merge_node = leaf; } else { return; }
if (bptree_merge_leaf(tree, merge_node, &parent_key_idx) != BPTREE_OK) return;
if (merge_node->parent && bptree_is_underflow(tree, merge_node->parent)) { bptree_fix_internal(tree, merge_node->parent); } }
|
⭐ 一个关键细节
merge_leaf 的参数必须是“左节点”
因此在 fix_leaf 中必须显式判断左右兄弟。
七、叶子合并:bptree_merge_leaf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| int bptree_merge_leaf(bptree* tree, bptree_node* left, int* out_parent_key_idx) { if (!left || !left->parent || !out_parent_key_idx) return BPTREE_ERR;
bptree_node* parent = left->parent; int idx = bptree_find_child_index(parent, left); if (idx < 0 || idx >= parent->key_count) return BPTREE_ERR;
bptree_node* right = parent->children[idx + 1]; if (!right) return BPTREE_ERR;
if (left->key_count + right->key_count > MAX_KEYS) return BPTREE_ERR;
int old = left->key_count; for (int i = 0; i < right->key_count; i++) { left->keys[old + i] = right->keys[i]; left->values[old + i] = right->values[i]; }
left->key_count += right->key_count; left->next = right->next;
*out_parent_key_idx = idx; return BPTREE_OK; }
|
📌 注意:这里不修改 parent,不 free right。
八、Internal 修复:bptree_fix_internal
逻辑和 leaf 完全对称:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void bptree_fix_internal(bptree* tree, bptree_node* node) { if (bptree_borrow_from_left_internal(node) == BPTREE_OK) return; if (bptree_borrow_from_right_internal(node) == BPTREE_OK) return;
int parent_key_idx = -1; bptree_node* left = bptree_get_left_sibling(node);
bptree_node* merge_node = left ? left : node;
if (bptree_merge_internal(tree, merge_node, &parent_key_idx) != BPTREE_OK) return;
if (merge_node->parent && bptree_is_underflow(tree, merge_node->parent)) { bptree_fix_internal(tree, merge_node->parent); } }
|
九、一个血泪教训:为什么 parent 不能在 merge 里删?
我一开始在 merge_internal 里写了:
1 2 3
| parent->keys[...] 移动 parent->children[...] 移动 parent->key_count--
|
结果是:
- parent 被删了 两次
- root 被错误 shrink
- 整棵树“看起来能跑,但结构已经坏了”
💡 最终结论:
parent 的 key 删除,必须集中在 delete_from_internal 里完成。
十、总结(如果你只记住三点)
1️⃣ merge 只合并,不动 parent
2️⃣ fix 只调度,不做具体删除
3️⃣ parent 的 key 删除只能有一个入口
如果你能做到这三点,B+ 树删除一定是稳定的。
后记
这份实现不是抄教材,而是一步一步踩坑踩出来的。
如果你也在手写 B+ 树,希望这篇文章能少让你 debug 几个晚上。
如果你愿意,我可以下一篇帮你整理:
- 🔥 B+ 树插入的“同一套设计哲学”
- 🔥 一张完整的删除流程调用图
- 🔥 或者把这套代码改成“竞赛 / 面试版精简实现”
欢迎继续交流。