【数据结构】B*树

一、基础定义与核心定位

1. 定义

B树是B 树的深度优化版本 ,由数据库领域学者为海量磁盘存储设计,核心目标:尽可能提升节点填充率,减少节点分裂次数,降低磁盘 IO 开销 标准 B 树节点填满后直接二分分裂,空间利用率平均仅 50%; B 树增加兄弟节点借键机制,只有左右兄弟全部塞满时才执行三分分裂,节点填充率稳定维持在 2/3 以上,磁盘空间利用率大幅提升。

2. m 阶 B * 树严格约束(对比 B 树、B + 树)

m 阶:单个节点最多容纳 m 个子节点,最多 m-1 个关键字

  1. 全局平衡规则:所有叶子节点处于同一层,完全平衡;
  2. 根节点:树高 > 1 时,至少拥有 2 个子节点;单节点树无下限;
  3. 非根、非叶子节点硬性下限 :最少 ⌈2m/3⌉ 个子节点,最少 ⌈2m/3⌉-1 个关键字; B 树下限是 ⌈m/2⌉,B * 树下限更高,节点不能太稀疏,空间更紧凑;
  4. 存储规则 :关键字 + 完整数据同时存放在叶子、非叶子节点,无叶子有序链表(和 B 树一致,区别于 B + 树);
  5. 分裂特殊规则:节点溢出时,优先向左右兄弟借关键字;兄弟无空闲容量时,三个节点(自身 + 左右兄弟)三分拆分,而非二分。

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]
  1. 每个 key 附带完整业务数据,非叶子、叶子都存数据;
  2. 无叶子双向链表,范围查询逻辑和 B 树一致,需要递归遍历子树;
  3. 子指针指向下层磁盘页,节点整体占用一个磁盘 IO 页。

三、三大核心操作:查找、插入、删除

1. 查找操作(逻辑与标准 B 树完全相同)

  1. 从根节点向下遍历,节点内关键字升序排列;
  2. 匹配到目标 key 直接返回数据;
  3. 未匹配则进入对应区间子树;
  4. 抵达叶子仍无匹配则查找失败。 优势:树高比同阶 B 树更低,磁盘 IO 次数更少。

2. 插入操作(B * 树最核心特色:先借后分、三分分裂)

完整流程
  1. 插入仅发生在叶子节点,向下遍历找到对应叶子写入 key;
  2. 插入后 key 数量未达 m 上限:操作结束;
  3. 插入后节点 key 溢出(等于 m),进入借键判断流程
    • 步骤 1:查看左 / 右兄弟节点是否有空余位置(key 数量 < m-1);
    • 步骤 2:若兄弟有空闲:兄弟分出一个 key 给当前节点,父节点分隔键同步调整,无需分裂,直接终止;
    • 步骤 3:左右兄弟全部填满无空闲:执行三分分裂(B 树是二分);
三分分裂完整规则(m 阶通用)

当前满节点 + 左 / 右满兄弟 + 父分隔键,三个节点重新均分所有关键字:

  1. 合并自身、兄弟、父节点分隔 key 为有序大数组;
  2. 平均分成 3 份,分别放入原节点、兄弟节点、新建第三个节点;
  3. 两个中间分界 key 提升到父节点;
  4. 父节点新增两个 key,若父节点因此溢出,递归向上执行借键 / 三分分裂;
  5. 若递归分裂至根节点,新建根,树高度 + 1。
实操举例:4 阶 B * 树(m=4,最多 3key,最少 2key)

叶子节点 [10,20,30](已满),插入 35 溢出:

  1. 查看右兄弟 [40,50,60] 同样填满,无法借键;
  2. 合并数组 [10,20,30,35,40,50,60]
  3. 三分拆分:左[10,20]、中[30,35]、右[40,50,60]
  4. 两个中间值 30、40 提升至父节点作为分隔索引;
  5. 父节点新增两个 key,完成分裂。

3. 删除操作(欠载优先借键,无资源则三节点合并)

删除后节点 key 数量低于下限 ⌈2m/3⌉-1(欠载),处理优先级:

  1. 优先向左右兄弟借关键字 兄弟节点 key 数量充足,分出一个 key 下沉至当前节点,父分隔键同步互换,修复节点数量;
  2. 兄弟也刚好达到最低下限,无多余 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);
}
  1. 初始化阶数m、单节点关键字上限max_keys = m-1
  2. 计算 B * 树专属下限:min_child = ceil(2.0*m/3),最少关键字min_keys = max(1, min_child-1)
  3. 创建初始空根节点(默认叶子)。

示例:4 阶 B * 树 m=4min_child=ceil(8/3)=3min_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 树分裂逻辑一致:

  1. 传入完整有序关键字数组;
  2. mid = total / 2(Base-0 下标),中间值作为上升分隔键;
  3. 左侧保留[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。 执行流程:

  1. 校验总关键字数量:至少min_seg*3 + 2才能三分;
  2. 计算两个分割下标pos1pos2,保证左 / 中 / 右三段关键字数量均≥最小下限;
  3. 拆分三段关键字,提取两个分隔键k1、k2
  4. 填充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 树完全一致:

  1. 从根向下遍历索引节点,lower_bound定位目标子树;
  2. 抵达叶子节点二分查找 key;
  3. 匹配成功返回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;
    }
}
整体流程
  1. 向下遍历寻叶 :栈stack记录路径(父节点 + 子树下标),重复键直接拦截;
  2. 叶子有序插入 key ,完成后循环校验节点是否溢出keys.size() > max_keys
  3. 溢出分层处理(优先级从高到低):
分支 1:当前节点是根节点

直接二分拆分,新建上层索引根,树高度 + 1,终止循环。

分支 2:非根节点,优先向左右兄弟借键(B * 树核心优化)
  • 左兄弟未满:从溢出节点头部挪一个 key 到左兄弟,同步更新父节点分隔键;非叶子同步迁移子树;修复完成,向上一层循环校验;
  • 右兄弟未满:从溢出节点尾部挪一个 key 到右兄弟,同步更新父节点分隔键;非叶子同步迁移子树;修复完成,向上一层循环校验;
分支 3:左右兄弟全部填满,尝试三分分裂
  1. 合并当前节点 + 相邻满兄弟 + 父分隔键为完整关键字数组;
  2. 调用try_split_three判断是否可三分均分;
  3. 可三分:拆分左 / 中 / 右三个节点,两个分隔键插入父节点,新增中间子节点;向上一层循环校验;
分支 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];
    }
}
完整删除流程
  1. 向下遍历定位 key,栈记录父节点路径;
  2. 分两种删除场景:
    • 场景 1:key 在叶子节点:直接删除目标 key;
    • 场景 2:key 在索引节点:取左子树最大值(前驱)替换当前 key,再去叶子删除该前驱值;
  3. 调用底层循环修复节点欠载(关键字数量 < min_keys)。
欠载修复循环逻辑(del内置 while 循环)

循环条件:当前节点不是根、关键字不足下限

  1. 取出父节点与当前子树下标,获取左右兄弟;
  2. 优先借键修复 (优先级最高)
    • 左兄弟关键字充足:向左兄弟借末尾 key,父分隔键互换,非叶子同步迁移子树;修复完成跳出循环;
    • 右兄弟关键字充足:向右兄弟借头部 key,父分隔键互换,非叶子同步迁移子树;修复完成跳出循环;
  3. 兄弟无富余 key,执行两节点合并
    • 合并当前节点、父分隔键、相邻兄弟节点所有关键字;
    • 二分重新均分两组关键字,更新父节点分隔键;
    • 删除多余子节点指针,父节点同步删除旧分隔键;
  4. 将父节点设为当前节点,继续循环校验上层是否欠载;
根节点特殊处理

删除后根节点无关键字、但存在子节点:将第一个子节点升级为新根,释放旧根内存。

(六)四大遍历函数

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.参考地址

GitCode:B-Tree-Visualizer/bstartree.cpp-代码预览-B-Tree-Visualizer:基于 Python+Tkinter+Matplotlib 的 B-树/B+/B*树可视化工具项目 - AtomGit

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,触发溢出; 当前是根节点,无兄弟,执行标准二分分裂:

  1. 数组长度len=4mid = 4//2 = 2(Base-0 下标 2),分隔键all_keys[2] = 25上移;
  2. 左节点保留keys[0:2] = [5,15]
  3. 右节点保留keys[3:] = [35]
  4. 新建非叶子根节点承载分隔键[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 * 树的优缺点

优点

  1. 空间利用率极高 强制节点至少填充 2/3,同数据量下节点总数远少于 B 树,磁盘占用更小;
  2. 分裂操作极少,减少磁盘写 IO 不会一满就分裂,优先复用兄弟节点容量,海量数据场景大幅降低磁盘写入开销;
  3. 树高度更低,查询 IO 更少 节点容纳关键字更多,树的层数压缩,等值查询磁盘读取次数优于标准 B 树。

缺点

  1. 范围查询性能差 无叶子有序链表,区间查询需要多层递归遍历子树,远不如 B + 树;
  2. 增删逻辑复杂,算法实现成本高 插入、删除都需要先判断兄弟节点容量,三分分裂、三节点合并逻辑繁琐;
  3. 不适合在线高频范围查询业务 互联网 OLTP 数据库不会选用,仅用于离线归档、海量冷存储系统。

八、底层磁盘设计逻辑(为什么海量存储选用 B * 树)

  1. 离线海量数据以磁盘持久化存储为主,磁盘写入速度远慢于读取,分裂会产生大量磁盘重写;
  2. B 树每填满就分裂,频繁生成新磁盘页,IO 开销巨大;
  3. B * 树延迟分裂,最大限度复用已有磁盘页,减少页面创建、拷贝、写入操作;
  4. 高填充率减少总节点数量,磁盘页占用更少,节省存储硬件成本。

九、高频面试考点

  1. B * 树和 B 树核心区别:节点填充下限更高、溢出先借后三分分裂;
  2. B * 树下限公式:⌈2m/3⌉,填充率最低 2/3;
  3. 为什么 MySQL 不用 B树? 数据库核心需求是等值查询 + 分页 / 范围查询,B + 树有序链表在区间查询碾压 B树;B * 树复杂分裂逻辑增加数据库锁竞争开销;
  4. B * 树分裂方式:三分分裂,一次性拆分 3 个节点,提升两个分隔键到父节点;
  5. 适用场景:离线海量归档存储,而非在线业务数据库。

十、总结

B*树是B树的优化版本,通过提高节点填充下限(≥2/3)和优先借键机制减少分裂次数,显著提升磁盘空间利用率。其核心特性包括:节点溢出时先向兄弟节点借键,仅当兄弟节点均满时才执行三分分裂;删除时优先借键修复欠载,否则合并三节点。相比B树,B树减少了约50%的分裂操作和磁盘IO,但范围查询性能较差(无叶子链表)。典型应用于海量离线存储系统,不适合高频范围查询的在线数据库。代码实现展示了4阶B*树的插入、删除及三分分裂等核心逻辑,通过延迟分裂策略实现更优的磁盘写入性能。

相关推荐
不如语冰7 小时前
AI大模型入门-模块导入import
数据结构·人工智能·pytorch·python
斐夷所非7 小时前
在纷繁竞逐中稳步前行:C++ 2006–2020
c++
岑梓铭7 小时前
《考研408数据结构》第七章(7.1 查找:顺序查找、折半查找、分块查找)复习笔记
数据结构·笔记·考研·408·ds·查找
壹号用户8 小时前
c++入门之list了解及使用
数据结构·list
QXWZ_IA8 小时前
**电力登高作业高空失保实时监管:千寻智能安全带高挂低用监测方案**
人工智能·科技·算法·能源·智能硬件
保持清醒5408 小时前
类和对象上
c++
来一碗刘肉面8 小时前
什么是双端队列
数据结构·链表
爱刷碗的苏泓舒8 小时前
FFRT:固定失败率比率检验、宽巷模糊度固定及 Ratio Test
算法·相位偏差·模糊度固定·ppp-ar·ffrt·ratio test·wlnl
小园子的小菜8 小时前
深入理解 JVM 垃圾回收:从对象判定、回收算法到经典收集器全解析
jvm·算法