因为红黑树单次操作虽为o(log n),但b-tree节点更宽、树高更低,能减少磁盘io和缓存不命中;手写核心动机是控制页大小、保证迭代器稳定、对接自定义存储层。

为什么不用 std::map 或 std::set 而要手写 B-tree?
因为它们底层是红黑树,单次操作 O(log n),但磁盘 IO 或大内存页场景下,B-tree 的节点更宽、树高更低,能显著减少比较次数和缓存不命中。手写 B-tree 的核心动机通常是:需要控制节点大小(比如匹配 4KB 页)、支持范围迭代器稳定性、或对接自定义存储层(如 mmap 文件)。
常见错误是直接照搬教科书伪代码,忽略 C++ 的内存管理细节——比如用裸指针管理子节点,没处理移动语义,导致析构时 double-free;或者把 std::vector 当作节点内键值容器却不预留空间,频繁重分配拖慢插入。
- 节点内键数组建议用
std::array(编译期确定大小)或带reserve()的std::vector - 子节点指针必须统一用
std::unique_ptr,禁止裸指针 +new/delete - 不要在节点里存
std::string等非 trivial 类型,除非显式处理移动/拷贝构造
BTree::insert() 怎么处理上溢和分裂?
分裂不是简单“把后半键值搬走”,关键在于:父节点插入新键时可能再次触发上溢,必须递归或用栈模拟向上修复。很多实现卡在这里——只处理当前层分裂,忘了更新父节点的子指针索引,导致后续查找跳到空地址。
典型错误现象:segmentation fault 出现在第二次插入后,gdb 显示访问了野指针;或查找返回错误值,因为某个内部节点的 children[2] 实际指向已释放内存。
- 分裂后,新节点的指针必须赋给父节点对应位置,且父节点键也要插入中间键(即原节点的第
t个键) - 若父节点也满,先递归分裂父节点,再把提升上来的键插入祖父节点
- 根节点上溢时需新建根,高度+1——这是唯一让树长高的操作,别漏掉
迭代器怎么保证遍历顺序和有效性?
B-tree 迭代器不是包装一个指针就行。它得记住当前在哪个节点、第几个键、以及从根到该节点的路径(用于 operator++ 找后继)。否则 ++it 可能跳过整个子树,或在删除后失效却没报错。
常见坑是迭代器持有原始指针,但节点被分裂/合并后内存重排,指针悬空;或者没实现 operator==,导致 for (auto it = begin(); it != end(); ++it) 死循环。
- 迭代器内部应存
std::shared_ptr<node></node>和索引,避免悬垂 - 后继查找逻辑:如果当前键有右子树,就去右子树最左叶子;否则沿父节点链向上,直到找到“来自左子树”的父键
- 不要在迭代器里缓存整个路径栈——开销大;改用轻量级
parent_stack在必要时重建
模板参数 T 和 Compare 有哪些实际约束?
教科书常假设 T 可拷贝,但真实场景中可能是不可拷贝的大对象(如含 std::mutex 的结构体)。这时必须支持移动语义,且 Compare 必须满足严格弱序,否则 find() 可能找不到已存在的键。
典型错误:用 std::less<:string_view></:string_view> 作为比较器,但节点里存的是 std::string,导致 find("abc") 因类型不匹配编译失败;或自定义 Compare 没标记 noexcept,在 std::vector::push_back 重分配时抛异常,破坏强异常安全。
- 节点内键容器建议用
std::vector<t></t>,而非std::array<t></t>,除非T是 trivial 类型 -
Compare必须可默认构造,且对任意两个合法T值返回 bool(不能断言或 throw) - 如果
T很大,考虑在节点中存std::unique_ptr<t></t>,但会增加解引用开销和 cache miss
真正难的不是算法逻辑,而是让每个节点的生命周期、迭代器的路径跟踪、以及模板实例化后的内存布局全部对齐。稍微松懈,就会在压力测试时出现偶发崩溃或迭代错位——这些 bug 往往只在特定数据分布和插入顺序下暴露。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











