一、基础定义与核心定位
1. 定义
B树是B 树的深度优化版本 ,由数据库领域学者为海量磁盘存储设计,核心目标:尽可能提升节点填充率,减少节点分裂次数,降低磁盘 IO 开销。 标准 B 树节点填满后直接二分分裂,空间利用率平均仅 50%; B 树增加兄弟节点借键机制,只有左右兄弟全部塞满时才执行三分分裂,节点填充率稳定维持在 2/3 以上,磁盘空间利用率大幅提升。
2. m 阶 B * 树严格约束(对比 B 树、B + 树)
m 阶:单个节点最多容纳 m 个子节点,最多 m-1 个关键字
- 全局平衡规则:所有叶子节点处于同一层,完全平衡;
- 根节点:树高 > 1 时,至少拥有 2 个子节点;单节点树无下限;
- 非根、非叶子节点硬性下限 :最少
⌈2m/3⌉个子节点,最少⌈2m/3⌉-1个关键字; B 树下限是⌈m/2⌉,B * 树下限更高,节点不能太稀疏,空间更紧凑; - 存储规则 :关键字 + 完整数据同时存放在叶子、非叶子节点,无叶子有序链表(和 B 树一致,区别于 B + 树);
- 分裂特殊规则:节点溢出时,优先向左右兄弟借关键字;兄弟无空闲容量时,三个节点(自身 + 左右兄弟)三分拆分,而非二分。
3. 直观对比:4 阶 B 树 vs 4 阶 B * 树
- 4 阶 B 树:最少 2 子树、1 关键字,填充底线 50%;
- 4 阶 B树:
⌈2*4/3⌉=3,最少 3 子树、2 关键字,填充底线 66.7%; 同样阶数下,B树节点必须存更多 key 才允许分裂,分裂频率大幅降低。
二、节点结构
和标准 B 树结构完全一致,无链表:
plaintext
[子指针0][key₀][data₀][子指针1][key₁][data₁]...[keyₙ₋₁][dataₙ₋₁][子指针n]
- 每个 key 附带完整业务数据,非叶子、叶子都存数据;
- 无叶子双向链表,范围查询逻辑和 B 树一致,需要递归遍历子树;
- 子指针指向下层磁盘页,节点整体占用一个磁盘 IO 页。
三、三大核心操作:查找、插入、删除
1. 查找操作(逻辑与标准 B 树完全相同)
- 从根节点向下遍历,节点内关键字升序排列;
- 匹配到目标 key 直接返回数据;
- 未匹配则进入对应区间子树;
- 抵达叶子仍无匹配则查找失败。 优势:树高比同阶 B 树更低,磁盘 IO 次数更少。
2. 插入操作(B * 树最核心特色:先借后分、三分分裂)
完整流程
- 插入仅发生在叶子节点,向下遍历找到对应叶子写入 key;
- 插入后 key 数量未达 m 上限:操作结束;
- 插入后节点 key 溢出(等于 m),进入借键判断流程 :
- 步骤 1:查看左 / 右兄弟节点是否有空余位置(key 数量 < m-1);
- 步骤 2:若兄弟有空闲:兄弟分出一个 key 给当前节点,父节点分隔键同步调整,无需分裂,直接终止;
- 步骤 3:左右兄弟全部填满无空闲:执行三分分裂(B 树是二分);
三分分裂完整规则(m 阶通用)
当前满节点 + 左 / 右满兄弟 + 父分隔键,三个节点重新均分所有关键字:
- 合并自身、兄弟、父节点分隔 key 为有序大数组;
- 平均分成 3 份,分别放入原节点、兄弟节点、新建第三个节点;
- 两个中间分界 key 提升到父节点;
- 父节点新增两个 key,若父节点因此溢出,递归向上执行借键 / 三分分裂;
- 若递归分裂至根节点,新建根,树高度 + 1。
实操举例:4 阶 B * 树(m=4,最多 3key,最少 2key)
叶子节点 [10,20,30](已满),插入 35 溢出:
- 查看右兄弟
[40,50,60]同样填满,无法借键; - 合并数组
[10,20,30,35,40,50,60]; - 三分拆分:左
[10,20]、中[30,35]、右[40,50,60]; - 两个中间值 30、40 提升至父节点作为分隔索引;
- 父节点新增两个 key,完成分裂。
3. 删除操作(欠载优先借键,无资源则三节点合并)
删除后节点 key 数量低于下限 ⌈2m/3⌉-1(欠载),处理优先级:
- 优先向左右兄弟借关键字 兄弟节点 key 数量充足,分出一个 key 下沉至当前节点,父分隔键同步互换,修复节点数量;
- 兄弟也刚好达到最低下限,无多余 key 当前节点 + 父分隔 key + 相邻兄弟节点 三节点合并为一个完整节点; 父节点删除对应的分隔 key,若父节点因此欠载,向上递归处理。
四、B*树的基本操作C++代码实现
本代码实现4 阶工业 Base-0 标准 B * 树 ,核心特性:节点溢出优先向兄弟借键,兄弟无空闲则三分分裂;删除欠载优先借键,无空间则两节点合并。
(一)前置结构体与类成员
1. 辅助拆分结构体
SplitTwoRes 二分拆分结果
用于普通二分分裂、删除两节点合并后重拆分:存储左关键字数组、中间提升分隔键、右关键字数组。
cpp
struct SplitTwoRes {
vector<int> left; // 左节点关键字
int sep; // 提升到父节点的中间分隔键
vector<int> right; // 右节点关键字
};
SplitThreeRes 三分拆分结果(B * 树独有核心)
三节点均分关键字,生成两个分隔键提升至父节点:左数组、第一个上升键、中间数组、第二个上升键、右数组。
cpp
struct SplitThreeRes {
vector<int> left;
int k1; // 第一个上移分隔键
vector<int> mid;
int k2; // 第二个上移分隔键
vector<int> right;
};
2. BStarNode B * 树节点结构体
cpp
struct BStarNode {
vector<int> keys; // 有序关键字数组
vector<BStarNode*> children;// 子节点指针,长度=keys.size()+1
bool leaf; // true=叶子,false=索引节点
BStarNode(bool isLeaf = true) : leaf(isLeaf) {}
};
B * 树与标准 B 树节点结构完全一致:叶子、索引节点均存储完整数据,无叶子双向链表。
3. BStarTree 类成员变量
cpp
int m; // B*树阶数
int max_keys; // 单节点最大关键字 m-1
int min_child; // 非根节点最少子节点数 ⌈2m/3⌉(B*树核心下限)
int min_keys; // 非根节点最少关键字 min_child - 1
BStarNode* root; // 根节点指针
构造函数 BStarTree(int order):
cpp
BStarTree(int order) {
m = order;
max_keys = m - 1;
min_child = static_cast<int>(ceil(2.0 * m / 3));
min_keys = max(1, min_child - 1);
root = new BStarNode(true);
}
- 初始化阶数
m、单节点关键字上限max_keys = m-1; - 计算 B * 树专属下限:
min_child = ceil(2.0*m/3),最少关键字min_keys = max(1, min_child-1); - 创建初始空根节点(默认叶子)。
示例:4 阶 B * 树 m=4,min_child=ceil(8/3)=3,min_keys=2 规则:非根节点最少 3 个子节点、2 个关键字,填充率≥66.7%,远高于标准 B 树 50%。
(二)拆分工具函数(B * 树核心底层)
1. split_two_even 标准二分拆分
cpp
SplitTwoRes split_two_even(const vector<int>& all) {
int total = all.size();
int mid = total / 2;
vector<int> left(all.begin(), all.begin() + mid);
int sep = all[mid];
vector<int> right(all.begin() + mid + 1, all.end());
return SplitTwoRes(left, sep, right);
}
通用二分逻辑,与标准 B 树分裂逻辑一致:
- 传入完整有序关键字数组;
mid = total / 2(Base-0 下标),中间值作为上升分隔键;- 左侧保留
[0,mid),右侧保留(mid, end],返回拆分结果。 使用场景:根节点溢出强制二分、删除合并后重新均分两节点。
2. try_split_three 三分拆分判断与均分(B * 树独有)
cpp
bool try_split_three(const vector<int>& all, SplitThreeRes& out) {
int min_seg = min_keys;
int min_total = min_seg * 3 + 2;
if ((int)all.size() < min_total)
return false;
int total = (int)all.size();
int pos1 = max(min_seg, total / 3);
int pos2 = min(total - min_seg - 1, 2 * total / 3);
if (pos2 - pos1 - 1 < min_seg)
return false;
vector<int> l(all.begin(), all.begin() + pos1);
int k1 = all[pos1];
vector<int> mid(all.begin() + pos1 + 1, all.begin() + pos2);
int k2 = all[pos2];
vector<int> r(all.begin() + pos2 + 1, all.end());
out = SplitThreeRes(l, k1, mid, k2, r);
return true;
}
触发条件 :当前满节点 + 相邻满兄弟节点,合并后总关键字足够均分三份,每份不少于min_keys。 执行流程:
- 校验总关键字数量:至少
min_seg*3 + 2才能三分; - 计算两个分割下标
pos1、pos2,保证左 / 中 / 右三段关键字数量均≥最小下限; - 拆分三段关键字,提取两个分隔键
k1、k2; - 填充
SplitThreeRes并返回true;不满足均分条件返回false。 作用:一次性拆分 3 个节点,向父节点插入 2 个分隔键,大幅延后分裂,减少磁盘 IO。
(三)查询相关函数
1. bool search(int key) 等值查找
cpp
bool search(int key) {
BStarNode* cur = root;
while (true) {
int i = lower_bound(cur->keys.begin(), cur->keys.end(), key) - cur->keys.begin();
if ((size_t)i < cur->keys.size() && cur->keys[i] == key)
return true;
if (cur->leaf)
return false;
cur = cur->children[i];
}
}
逻辑与标准 B 树完全一致:
- 从根向下遍历索引节点,
lower_bound定位目标子树; - 抵达叶子节点二分查找 key;
- 匹配成功返回
true,遍历至叶子无匹配返回false。 特性:B * 树所有节点均存数据,非叶子节点可直接命中 key,提前终止查找。
2. get_min() / get_max() 全局最值
cpp
int get_min() {
BStarNode* cur = root;
while (!cur->leaf)
cur = cur->children[0];
return cur->keys[0];
}
int get_max() {
BStarNode* cur = root;
while (!cur->leaf)
cur = cur->children.back();
return cur->keys.back();
}
get_min():持续遍历最左子节点至叶子,返回叶子第一个 key;get_max():持续遍历最右子节点至叶子,返回叶子最后一个 key。
3. range_query(int l, int h) 区间查询
cpp
void range_dfs(BStarNode* n, int l, int h, vector<int>& res) {
if (n->leaf) {
for (int k : n->keys) {
if (k >= l && k <= h)
res.push_back(k);
}
return;
}
for (int i = 0; (size_t)i < n->keys.size(); i++) {
range_dfs(n->children[i], l, h, res);
if (n->keys[i] >= l && n->keys[i] <= h)
res.push_back(n->keys[i]);
}
range_dfs(n->children.back(), l, h, res);
}
vector<int> range_query(int l, int h) {
vector<int> res;
range_dfs(root, l, h, res);
return res;
}
递归 DFS 遍历整棵树,收集区间内所有关键字;无叶子链表,区间查询性能弱于 B + 树,和标准 B 树持平。
(四)插入核心函数 insert(int key)(B * 树特色逻辑集中处)
cpp
void insert(int key) {
vector<pair<BStarNode*, int>> stack;
BStarNode* cur = root;
while (!cur->leaf) {
int i = lower_bound(cur->keys.begin(), cur->keys.end(), key) - cur->keys.begin();
if ((size_t)i < cur->keys.size() && cur->keys[i] == key) {
cout << "重复键\n";
return;
}
stack.emplace_back(cur, i);
cur = cur->children[i];
}
int pos = lower_bound(cur->keys.begin(), cur->keys.end(), key) - cur->keys.begin();
if ((size_t)pos < cur->keys.size() && cur->keys[pos] == key) {
cout << "重复键\n";
return;
}
cur->keys.insert(cur->keys.begin() + pos, key);
while ((int)cur->keys.size() > max_keys) {
if (cur == root) {
int mid = (int)cur->keys.size() / 2;
int midk = cur->keys[mid];
BStarNode* l = new BStarNode(cur->leaf);
BStarNode* r = new BStarNode(cur->leaf);
l->keys.assign(cur->keys.begin(), cur->keys.begin() + mid);
r->keys.assign(cur->keys.begin() + mid + 1, cur->keys.end());
if (!cur->leaf) {
l->children.assign(cur->children.begin(), cur->children.begin() + mid + 1);
r->children.assign(cur->children.begin() + mid + 1, cur->children.end());
}
BStarNode* new_root = new BStarNode(false);
new_root->keys.push_back(midk);
new_root->children = {l, r};
root = new_root;
break;
}
pair<BStarNode*, int> item = stack.back();
stack.pop_back();
BStarNode* p = item.first;
int idx = item.second;
BStarNode* left_sib = (idx > 0) ? p->children[idx - 1] : nullptr;
BStarNode* right_sib = ((size_t)(idx + 1) < p->children.size()) ? p->children[idx + 1] : nullptr;
if (left_sib && (int)left_sib->keys.size() < max_keys) {
int sep = p->keys[idx - 1];
int move = cur->keys[0];
cur->keys.erase(cur->keys.begin());
left_sib->keys.push_back(sep);
p->keys[idx - 1] = move;
if (!cur->leaf) {
BStarNode* c = cur->children[0];
cur->children.erase(cur->children.begin());
left_sib->children.push_back(c);
}
cur = p;
continue;
}
if (right_sib && (int)right_sib->keys.size() < max_keys) {
int sep = p->keys[idx];
int move = cur->keys.back();
cur->keys.pop_back();
right_sib->keys.insert(right_sib->keys.begin(), sep);
p->keys[idx] = move;
if (!cur->leaf) {
BStarNode* c = cur->children.back();
cur->children.pop_back();
right_sib->children.insert(right_sib->children.begin(), c);
}
cur = p;
continue;
}
SplitThreeRes split3({}, 0, {}, 0, {});
bool can_split3 = false;
if (left_sib) {
vector<int> allk = left_sib->keys;
allk.push_back(p->keys[idx - 1]);
allk.insert(allk.end(), cur->keys.begin(), cur->keys.end());
can_split3 = try_split_three(allk, split3);
if (can_split3) {
left_sib->keys = split3.left;
BStarNode* mid_node = new BStarNode(cur->leaf);
mid_node->keys = split3.mid;
cur->keys = split3.right;
if (!cur->leaf) {
vector<BStarNode*> allc = left_sib->children;
allc.insert(allc.end(), cur->children.begin(), cur->children.end());
int s1 = (int)split3.left.size() + 1;
int s2 = s1 + (int)split3.mid.size() + 1;
left_sib->children.assign(allc.begin(), allc.begin() + s1);
mid_node->children.assign(allc.begin() + s1, allc.begin() + s2);
cur->children.assign(allc.begin() + s2, allc.end());
}
p->keys.erase(p->keys.begin() + idx - 1);
p->keys.insert(p->keys.begin() + idx - 1, split3.k2);
p->keys.insert(p->keys.begin() + idx - 1, split3.k1);
p->children.insert(p->children.begin() + idx, mid_node);
cur = p;
continue;
}
}
if (right_sib && !can_split3) {
vector<int> allk = cur->keys;
allk.push_back(p->keys[idx]);
allk.insert(allk.end(), right_sib->keys.begin(), right_sib->keys.end());
can_split3 = try_split_three(allk, split3);
if (can_split3) {
cur->keys = split3.left;
BStarNode* mid_node = new BStarNode(cur->leaf);
mid_node->keys = split3.mid;
right_sib->keys = split3.right;
if (!cur->leaf) {
vector<BStarNode*> allc = cur->children;
allc.insert(allc.end(), right_sib->children.begin(), right_sib->children.end());
int s1 = (int)split3.left.size() + 1;
int s2 = s1 + (int)split3.mid.size() + 1;
cur->children.assign(allc.begin(), allc.begin() + s1);
mid_node->children.assign(allc.begin() + s1, allc.begin() + s2);
right_sib->children.assign(allc.begin() + s2, allc.end());
}
p->keys.erase(p->keys.begin() + idx);
p->keys.insert(p->keys.begin() + idx, split3.k2);
p->keys.insert(p->keys.begin() + idx, split3.k1);
p->children.insert(p->children.begin() + idx + 1, mid_node);
cur = p;
continue;
}
}
int mid = (int)cur->keys.size() / 2;
int midk = cur->keys[mid];
BStarNode* rnode = new BStarNode(cur->leaf);
rnode->keys.assign(cur->keys.begin() + mid + 1, cur->keys.end());
cur->keys.resize((size_t)mid);
if (!cur->leaf) {
rnode->children.assign(cur->children.begin() + mid + 1, cur->children.end());
cur->children.resize((size_t)(mid + 1));
}
p->keys.insert(p->keys.begin() + idx, midk);
p->children.insert(p->children.begin() + idx + 1, rnode);
cur = p;
}
}
整体流程
- 向下遍历寻叶 :栈
stack记录路径(父节点 + 子树下标),重复键直接拦截; - 叶子有序插入 key ,完成后循环校验节点是否溢出
keys.size() > max_keys; - 溢出分层处理(优先级从高到低):
分支 1:当前节点是根节点
直接二分拆分,新建上层索引根,树高度 + 1,终止循环。
分支 2:非根节点,优先向左右兄弟借键(B * 树核心优化)
- 左兄弟未满:从溢出节点头部挪一个 key 到左兄弟,同步更新父节点分隔键;非叶子同步迁移子树;修复完成,向上一层循环校验;
- 右兄弟未满:从溢出节点尾部挪一个 key 到右兄弟,同步更新父节点分隔键;非叶子同步迁移子树;修复完成,向上一层循环校验;
分支 3:左右兄弟全部填满,尝试三分分裂
- 合并当前节点 + 相邻满兄弟 + 父分隔键为完整关键字数组;
- 调用
try_split_three判断是否可三分均分; - 可三分:拆分左 / 中 / 右三个节点,两个分隔键插入父节点,新增中间子节点;向上一层循环校验;
分支 4:无法三分,降级标准二分分裂
执行split_two_even二分拆分,一个分隔键插入父节点,新增右子节点;向上一层循环校验。
核心优势
普通 B 树节点填满立刻分裂;B * 树优先复用兄弟节点容量,仅当整条兄弟链全部填满才分裂,节点总数更少、磁盘写入次数大幅降低。
(五)删除全套函数 del(int key) + 欠载修复逻辑
cpp
void del(int key) {
vector<pair<BStarNode*, int>> stack;
BStarNode* cur = root;
int pos;
while (true) {
pos = lower_bound(cur->keys.begin(), cur->keys.end(), key) - cur->keys.begin();
if ((size_t)pos < cur->keys.size() && cur->keys[pos] == key)
break;
if (cur->leaf) {
cout << "不存在\n";
return;
}
stack.emplace_back(cur, pos);
cur = cur->children[pos];
}
int del_key;
if (!cur->leaf) {
BStarNode* pred = cur->children[pos];
stack.emplace_back(cur, pos);
while (!pred->leaf) {
stack.emplace_back(pred, (int)pred->keys.size());
pred = pred->children.back();
}
del_key = pred->keys.back();
cur->keys[pos] = del_key;
cur = pred;
} else {
del_key = key;
}
auto it = lower_bound(cur->keys.begin(), cur->keys.end(), del_key);
cur->keys.erase(it);
while (cur != root && (int)cur->keys.size() < min_keys) {
pair<BStarNode*, int> item = stack.back();
stack.pop_back();
BStarNode* p = item.first;
int idx = item.second;
BStarNode* left_sib = (idx > 0) ? p->children[idx - 1] : nullptr;
BStarNode* right_sib = ((size_t)(idx + 1) < p->children.size()) ? p->children[idx + 1] : nullptr;
if (left_sib && (int)left_sib->keys.size() > min_keys) {
int sep = p->keys[idx - 1];
int borrow = left_sib->keys.back();
left_sib->keys.pop_back();
cur->keys.insert(cur->keys.begin(), sep);
p->keys[idx - 1] = borrow;
if (!cur->leaf) {
BStarNode* c = left_sib->children.back();
left_sib->children.pop_back();
cur->children.insert(cur->children.begin(), c);
}
cur = p;
continue;
}
if (right_sib && (int)right_sib->keys.size() > min_keys) {
int sep = p->keys[idx];
int borrow = right_sib->keys[0];
right_sib->keys.erase(right_sib->keys.begin());
cur->keys.push_back(sep);
p->keys[idx] = borrow;
if (!cur->leaf) {
BStarNode* c = right_sib->children[0];
right_sib->children.erase(right_sib->children.begin());
cur->children.push_back(c);
}
cur = p;
continue;
}
vector<int> allk;
SplitTwoRes res({}, 0, {});
if (left_sib) {
allk = left_sib->keys;
allk.push_back(p->keys[idx - 1]);
allk.insert(allk.end(), cur->keys.begin(), cur->keys.end());
res = split_two_even(allk);
left_sib->keys = res.left;
cur->keys = res.right;
p->keys.erase(p->keys.begin() + idx - 1);
p->keys.insert(p->keys.begin() + idx - 1, res.sep);
if (!cur->leaf) {
vector<BStarNode*> allc = left_sib->children;
allc.insert(allc.end(), cur->children.begin(), cur->children.end());
int split_idx = (int)res.left.size() + 1;
left_sib->children.assign(allc.begin(), allc.begin() + split_idx);
cur->children.assign(allc.begin() + split_idx, allc.end());
}
p->children.erase(p->children.begin() + idx);
} else {
allk = cur->keys;
allk.push_back(p->keys[idx]);
allk.insert(allk.end(), right_sib->keys.begin(), right_sib->keys.end());
res = split_two_even(allk);
cur->keys = res.left;
right_sib->keys = res.right;
p->keys.erase(p->keys.begin() + idx);
p->keys.insert(p->keys.begin() + idx, res.sep);
if (!cur->leaf) {
vector<BStarNode*> allc = cur->children;
allc.insert(allc.end(), right_sib->children.begin(), right_sib->children.end());
int split_idx = (int)res.left.size() + 1;
cur->children.assign(allc.begin(), allc.begin() + split_idx);
right_sib->children.assign(allc.begin() + split_idx, allc.end());
}
p->children.erase(p->children.begin() + idx + 1);
}
cur = p;
}
if (root->keys.empty() && !root->children.empty()) {
root = root->children[0];
}
}
完整删除流程
- 向下遍历定位 key,栈记录父节点路径;
- 分两种删除场景:
- 场景 1:key 在叶子节点:直接删除目标 key;
- 场景 2:key 在索引节点:取左子树最大值(前驱)替换当前 key,再去叶子删除该前驱值;
- 调用底层循环修复节点欠载(关键字数量 < min_keys)。
欠载修复循环逻辑(del内置 while 循环)
循环条件:当前节点不是根、关键字不足下限
- 取出父节点与当前子树下标,获取左右兄弟;
- 优先借键修复 (优先级最高)
- 左兄弟关键字充足:向左兄弟借末尾 key,父分隔键互换,非叶子同步迁移子树;修复完成跳出循环;
- 右兄弟关键字充足:向右兄弟借头部 key,父分隔键互换,非叶子同步迁移子树;修复完成跳出循环;
- 兄弟无富余 key,执行两节点合并
- 合并当前节点、父分隔键、相邻兄弟节点所有关键字;
- 二分重新均分两组关键字,更新父节点分隔键;
- 删除多余子节点指针,父节点同步删除旧分隔键;
- 将父节点设为当前节点,继续循环校验上层是否欠载;
根节点特殊处理
删除后根节点无关键字、但存在子节点:将第一个子节点升级为新根,释放旧根内存。
(六)四大遍历函数
1. traversal_in() 中序遍历
cpp
// 中序遍历
void inorder(BStarNode* n, vector<int>& res) {
if (!n) return;
if (n->leaf) {
// 叶子直接输出key,不访问children
for (int k : n->keys)
res.push_back(k);
return;
}
// 非叶子才递归子节点
for (int i = 0; (size_t)i < n->keys.size(); i++) {
inorder(n->children[i], res);
res.push_back(n->keys[i]);
}
inorder(n->children.back(), res);
}
vector<int> traversal_in() {
vector<int> r;
inorder(root, r);
return r;
}
递归规则与标准 B 树一致,输出严格升序序列:
- 非叶子:递归子树 i → 输出 keys i → 递归最后一个子树;
- 叶子:直接输出全部关键字; 输出结果全局有序,是校验树正确性的核心手段。
2. traversal_pre() 前序遍历
cpp
void preorder(BStarNode* n, vector<int>& res) {
if (!n) return;
res.insert(res.end(), n->keys.begin(), n->keys.end());
for (BStarNode* c : n->children)
preorder(c, res);
}
vector<int> traversal_pre() {
vector<int> r;
preorder(root, r);
return r;
}
先输出当前节点所有关键字,再依次递归全部子节点。
3. traversal_post() 后序遍历
cpp
void postorder(BStarNode* n, vector<int>& res) {
if (!n) return;
for (BStarNode* c : n->children)
postorder(c, res);
res.insert(res.end(), n->keys.begin(), n->keys.end());
}
vector<int> traversal_post() {
vector<int> r;
postorder(root, r);
return r;
}
先递归所有子节点,再输出当前节点关键字。
4. traversal_level() 层序遍历
cpp
vector<int> traversal_level() {
vector<int> r;
queue<BStarNode*> q;
q.push(root);
while (!q.empty()) {
BStarNode* u = q.front();
q.pop();
r.insert(r.end(), u->keys.begin(), u->keys.end());
for (BStarNode* c : u->children)
q.push(c);
}
return r;
}
队列广度优先,从上到下、每层从左至右打印节点关键字,直观分层展示树结构。
(七)C++完整代码参考地址与运行结果完整展示
1.参考地址
GitHub:B-Tree-Visualizer/bstartree.cpp at main · hongyuxu0/B-Tree-Visualizer · GitHub
2.运行结果完整展示

五、完整实操示例:4 阶 Base-0 B* 树 插入全过程
初始空树,依次插入:5,15,25,35,45,10,8
步骤 1:依次插入 5、15、25
初始空树,根为叶子节点,连续插入 3 个键,刚好达到上限max_keys=3,无溢出。 节点:[5, 15, 25](根 = 叶子,3 个 key,已满)
树结构:

步骤 2:插入 35 → 当前节点溢出,无兄弟,根直接二分分裂
原叶子[5,15,25,35],键数 = 4 > 3,触发溢出; 当前是根节点,无兄弟,执行标准二分分裂:
- 数组长度
len=4,mid = 4//2 = 2(Base-0 下标 2),分隔键all_keys[2] = 25上移; - 左节点保留
keys[0:2] = [5,15]; - 右节点保留
keys[3:] = [35]; - 新建非叶子根节点承载分隔键
[25],两个子节点分别指向左、右叶子。
分裂后完整树:

节点容量校验:
- 左叶子
[5,15]:2 个 key = min_keys,合法; - 右叶子
[35]:1 个 key,根的子节点不受 min_keys 限制,合规。
步骤 3:插入 45 → 写入右侧叶子,无溢出
右叶子[35]插入 45,有序变为[35, 45],键数 = 2,未达上限 3,稳定无操作。
当前树:

步骤 4:插入 10 → 写入左侧叶子,填满至上限
左叶子[5,15]插入 10,有序排序[5, 10, 15],刚好 3 个 key,达到 max_keys,无溢出。
当前树:

左叶子已满(3key),右叶子未满(2key)。
步骤 5:插入 8 → 左叶子溢出,优先尝试向兄弟借键(B * 树核心逻辑)
5.1 溢出判断
左叶子[5,10]插入8,有序排序得到[5,8,10],键数 = 3,刚好等于 max_keys=3,没有溢出!
重分配后最终完整树结构

六、B 树 / B + 树 / B * 树 全方位对比
| 对比维度 | 标准 B 树 | B + 树 | B * 树 |
|---|---|---|---|
| 数据存储位置 | 叶子、非叶子均存完整数据 | 仅叶子存数据,上层只有索引 key | 叶子、非叶子均存完整数据 |
| 叶子链表 | 无 | 双向有序链表 | 无 |
| 节点最低填充下限 | ⌈m/2⌉-1(50%) | ⌈m/2⌉-1 | ⌈2m/3⌉-1(66.7%) |
| 溢出分裂策略 | 直接二分拆分 2 个节点 | 叶子二分拆分 | 先向兄弟借键;无法借则三分拆 3 节点 |
| 范围查询效率 | 差,频繁切换子树 | 极高,链表顺序遍历 | 差,同 B 树 |
| 磁盘分裂 IO 次数 | 多,填满即分裂 | 中等 | 极少,大幅延后分裂 |
| 典型应用场景 | 小型嵌入式文件索引 | MySQL、PostgreSQL 数据库索引 | 海量离线数据、分布式持久化存储 |
七、B * 树的优缺点
优点
- 空间利用率极高 强制节点至少填充 2/3,同数据量下节点总数远少于 B 树,磁盘占用更小;
- 分裂操作极少,减少磁盘写 IO 不会一满就分裂,优先复用兄弟节点容量,海量数据场景大幅降低磁盘写入开销;
- 树高度更低,查询 IO 更少 节点容纳关键字更多,树的层数压缩,等值查询磁盘读取次数优于标准 B 树。
缺点
- 范围查询性能差 无叶子有序链表,区间查询需要多层递归遍历子树,远不如 B + 树;
- 增删逻辑复杂,算法实现成本高 插入、删除都需要先判断兄弟节点容量,三分分裂、三节点合并逻辑繁琐;
- 不适合在线高频范围查询业务 互联网 OLTP 数据库不会选用,仅用于离线归档、海量冷存储系统。
八、底层磁盘设计逻辑(为什么海量存储选用 B * 树)
- 离线海量数据以磁盘持久化存储为主,磁盘写入速度远慢于读取,分裂会产生大量磁盘重写;
- B 树每填满就分裂,频繁生成新磁盘页,IO 开销巨大;
- B * 树延迟分裂,最大限度复用已有磁盘页,减少页面创建、拷贝、写入操作;
- 高填充率减少总节点数量,磁盘页占用更少,节省存储硬件成本。
九、高频面试考点
- B * 树和 B 树核心区别:节点填充下限更高、溢出先借后三分分裂;
- B * 树下限公式:
⌈2m/3⌉,填充率最低 2/3; - 为什么 MySQL 不用 B树? 数据库核心需求是等值查询 + 分页 / 范围查询,B + 树有序链表在区间查询碾压 B树;B * 树复杂分裂逻辑增加数据库锁竞争开销;
- B * 树分裂方式:三分分裂,一次性拆分 3 个节点,提升两个分隔键到父节点;
- 适用场景:离线海量归档存储,而非在线业务数据库。
十、总结
B*树是B树的优化版本,通过提高节点填充下限(≥2/3)和优先借键机制减少分裂次数,显著提升磁盘空间利用率。其核心特性包括:节点溢出时先向兄弟节点借键,仅当兄弟节点均满时才执行三分分裂;删除时优先借键修复欠载,否则合并三节点。相比B树,B树减少了约50%的分裂操作和磁盘IO,但范围查询性能较差(无叶子链表)。典型应用于海量离线存储系统,不适合高频范围查询的在线数据库。代码实现展示了4阶B*树的插入、删除及三分分裂等核心逻辑,通过延迟分裂策略实现更优的磁盘写入性能。