【数据结构】B+树

一、B+树的基本概念

1. 定义

B + 树是B 树的改良分支 ,专为数据库、文件系统索引设计,是 MySQL、PostgreSQL 等数据库的底层主键索引标准结构。 核心设计目标:大幅优化区间范围查询、全量遍历查询,同时保持极低磁盘 IO 次数

2. m 阶 B + 树严格约束(和 B 树最大区别)

阶数 m 定义:节点最多存放 m 个子指针,最多 m-1 个索引关键字
  1. 非叶子节点(索引层)
    • 关键字仅做路由索引,不存储完整业务数据
    • 除根节点外,最少 ⌈m/2⌉ 个子节点,最少 ⌈m/2⌉-1 个 key;
    • 关键字与子指针规则:k₀ < 子树1 < k₁ < 子树2 < ...,和 B 树逻辑一致。
  2. 叶子节点(数据层,最核心区别)
    • 所有真实完整数据记录只存在叶子节点,非叶子只有索引;
    • 叶子节点之间通过双向有序链表串联,按 key 升序排列;
    • 叶子节点 key 数量和数据记录一一对应,区间查询只需顺着链表遍历;
  3. 全局平衡规则:所有叶子节点严格在同一层,树高度完全统一;
  4. 根节点特殊:树高度 > 1 时,至少 2 个子节点;单节点树无限制。

3. 和标准 B 树最直观差异对照表

特性 B 树 B + 树
数据存储位置 叶子、非叶子节点都存完整数据 仅叶子节点存完整数据,上层只有索引 key
叶子节点关系 无链表,互相独立 双向有序链表串联,全局升序
等值查询路径 可能在非叶子节点命中,提前结束 无论查找任何 key,必须走到叶子节点
范围查询 需要多次回溯父节点切换子树,IO 多 定位区间起点后,顺着链表遍历,IO 极少
全表扫描 整棵树深度遍历,开销极大 直接遍历叶子链表,连续读取磁盘页
节点填充 上下层都存放数据,空间分散 上层纯索引,一页能存更多路由,树更低

二、B + 树节点结构拆解

1. 非叶子节点(索引页,只用来导航)

bash 复制代码
[子指针0][key₀][子指针1][key₁][子指针2]...[keyₙ₋₁][子指针n]
  1. key:真实主键 / 索引字段;
  2. data:完整行数据(或磁盘行偏移地址);
  3. 尾部两个指针:prev 上一个叶子、next 下一个叶子,形成有序双向链表。

结构示意图(4 阶 B + 树 m=4)

所有叶子节点首尾相连,全局升序。

三、四大核心操作:查找 / 插入 / 删除 / 分裂合并

1. 查找操作(等值查询)

流程
  1. 从根索引节点逐层匹配分界 key,找到对应下层子树;
  2. 一直向下遍历,必然抵达叶子节点
  3. 在叶子有序数组内二分查找目标 key,匹配则取出 data,无匹配则查找失败。
示例:查找 22
  1. 根节点 [10,30] → 22 介于 10、30 之间,进入中间子树 [15,22]
  2. 索引节点 [15,22] → 22 等于分界 key,进入右侧叶子 [20,22]
  3. 叶子内找到 22,读取对应数据。
范围查询(B + 树碾压 B 树的核心场景)

需求:查询 12 ≤ key ≤ 35

  1. 向下遍历找到区间起点 12 所在叶子;
  2. 顺着叶子节点的 next 链表依次读取:[12,15] → [20,22] → [32,35]
  3. 无需向上回溯、切换分支,连续磁盘顺序读取,性能极高。

2. 插入操作(节点分裂规则)

前置规则
  1. 插入永远落在叶子节点,上层索引节点仅同步分界 key;
  2. 叶子插入 key 后,key 总数 ≤ m-1:直接写入,结束;
  3. 叶子 key 数量达到 m(溢出),执行叶子分裂
  4. 分裂后向上更新父索引节点,父节点溢出则递归向上分裂。
4 阶 B + 树分裂完整步骤(m=4,最多 3 个 key)

叶子原有 [20,22,32],插入 35,满 4 个 key 溢出:

  1. 合并有序数组 [20,22,32,35]
  2. 从中间切割:左叶子 [20,22],右叶子 [32,35]
  3. 右叶子第一个 key(32) 提升到父索引节点作为分界值;
  4. 父节点新增 key=32,若父节点 key 数量达到 m,重复分裂逻辑;
  5. 若分裂递归到根节点,新建根索引,树高度 + 1。

关键区别 B 树:B 树提升中间 key,B + 树提升分裂后右叶子第一个 key。

3. 删除操作(借值 / 合并逻辑)

删除仅会直接修改叶子节点,上层索引仅做同步更新,分两类场景:

场景 1:删除后叶子 key 数量 ≥ 最低下限 ⌈m/2⌉-1

直接删除当前 key,若删除的 key 是父节点分界值,无需立刻修改上层(索引允许存在已删除的 key,仅作路由)。

场景 2:删除后叶子 key 不足下限(欠载)

方案 A(优先):向左右相邻叶子借关键字 相邻叶子 key 数量充足,取相邻的一个 key 移入当前叶子,同时更新父节点分界 key。

方案 B:相邻叶子无多余 key,执行节点合并 当前叶子 + 相邻叶子合并为一个新叶子,删除父节点对应的分界 key; 若父节点因此欠载,递归向上处理索引层。

特殊:索引层 key 冗余特性

B + 树上层索引的 key 可以在叶子不存在,不影响查询正确性,仅作为路由标记,因此删除叶子数据后不会立刻同步清理上层索引,减少修改开销。

四、B+ 树的基本操作C++代码完整实现

本代码实现标准 m 阶 B + 树,完全采用工业 0 下标(Base-0)开发逻辑,区分索引节点、叶子双向链表,实现插入分裂、删除借键 / 合并、区间查询、双版本中序遍历等全套功能。

(一)结构体与类成员变量

1. struct BPlusNode B + 树节点
cpp 复制代码
struct BPlusNode {
    vector<int> keys;          // 关键字数组,有序升序
    vector<BPlusNode*> children;// 子节点指针数组,长度=keys.size()+1
    bool leaf;                 // true=叶子节点(存完整数据);false=索引节点(仅分界key)
    BPlusNode *prev, *next;    // 叶子节点双向链表前驱、后继指针,索引节点无意义
    BPlusNode(bool isLeaf = true) : leaf(isLeaf), prev(nullptr), next(nullptr) {}
};

核心区别 B 树:

  • 叶子节点额外维护 prev/next 双向指针,串联全局有序链表;
  • 索引节点仅存分界索引,真实数据全部保存在叶子。
2. class BPlusTree 成员变量
cpp 复制代码
int m;               // B+树阶数
int max_keys;        // 单节点最大关键字 m-1
int min_keys;        // 非根节点最小关键字 ⌈m/2⌉-1 = (m-1)/2
BPlusNode* root;     // 根节点
BPlusNode* first_leaf;// 叶子链表头指针,快速遍历全量数据

构造函数 BPlusTree(int order)

cpp 复制代码
BPlusTree(int order) {
    m = order;
    max_keys = m - 1;
    min_keys = (m - 1) / 2;
    root = new BPlusNode(true);
    first_leaf = root;
}
  1. 初始化阶数、节点容量上下限;
  2. 创建空根(初始为叶子);
  3. first_leaf 指向根,作为叶子链表起点。

(二)叶子链表工具函数(B + 树独有核心)

1. BPlusNode* get_leftmost(BPlusNode* n) 获取最左叶子
cpp 复制代码
BPlusNode* get_leftmost(BPlusNode* n) {
    while (!n->leaf) n = n->children[0];
    return n;
}

功能:输入任意节点,一路向左遍历到最底层第一个叶子节点

执行流程:

循环判断 !n->leaf,持续取 children[0],直到抵达叶子,返回该叶子指针。

用途:初始化、根分裂后刷新 first_leaf 头指针。

cpp 复制代码
void link_leaf(BPlusNode* left, BPlusNode* right) {
    right->prev = left;
    right->next = left->next;
    if (left->next) left->next->prev = right;
    left->next = right;
}

场景:叶子分裂时,新建右叶子需要插入双向链表

执行流程:

  1. 右叶子前驱 = 原左节点;
  2. 右叶子后继 = 原左节点原本的后继;
  3. 如果左节点原有后继,修改其后继的前驱指向新右节点;
  4. 左节点后继指向新右节点; 完成双向链表插入。
cpp 复制代码
void unlink_leaf(BPlusNode* node) {
    if (node->prev) node->prev->next = node->next;
    if (node->next) node->next->prev = node->prev;
    if (node == first_leaf) first_leaf = node->next;
    node->prev = node->next = nullptr;
}

场景:叶子节点合并时,删除其中一个叶子,断开链表链接

执行流程:

  1. 若存在前驱,前驱后继指向 node 的后继;
  2. 若存在后继,后继前驱指向 node 的前驱;
  3. 如果当前节点是链表头 first_leaf,更新头指针为下一个叶子;
  4. 清空 node 的 prev/next 指针,脱离链表。

(三)查询相关函数

1. bool search(int key) 等值查找
cpp 复制代码
bool search(int key) {
    BPlusNode* cur = root;
    while (!cur->leaf) {
        int i = 0;
        while ((size_t)i < cur->keys.size() && key >= cur->keys[i]) {
            i++;
        }
        cur = cur->children[i];
    }
    auto it = lower_bound(cur->keys.begin(), cur->keys.end(), key);
    return (it != cur->keys.end() && *it == key);
}

B + 树特性:无论 key 是否匹配索引,必须走到叶子节点才能判断是否存在

执行流程:

  1. cur = root,循环向下遍历索引层:
    • 遍历当前节点 keys,找到第一个≥key 的分界下标i
    • 进入 children[i] 子节点;
  2. 抵达叶子节点后,lower_bound 二分查找 key;
  3. 匹配成功返回 true,否则 false。
2. get_min() / get_max() 全局最值
cpp 复制代码
int get_min() {
    return first_leaf->keys[0];
}
int get_max() {
    BPlusNode* p = first_leaf;
    while (p->next) p = p->next;
    return p->keys.back();
}
  • get_min():直接取 first_leaf 第一个 key,全局最小值;
  • get_max():从链表头一路向后遍历到最后一个叶子,取末尾 key。
3. vector<int> range_query(int low, int high) 区间查询(B + 树优势)
cpp 复制代码
vector<int> range_query(int low, int high) {
    vector<int> res;
    BPlusNode* cur = root;
    while (!cur->leaf) {
        int i = 0;
        while ((size_t)i < cur->keys.size() && low >= cur->keys[i]) {
            i++;
        }
        cur = cur->children[i];
    }
    while (cur) {
        for (int k : cur->keys) {
            if (k > high) return res;
            if (k >= low) res.push_back(k);
        }
        cur = cur->next;
    }
    return res;
}

利用叶子有序链表,无需递归回溯子树,顺序遍历即可

执行流程:

  1. 向下遍历索引层,找到区间下限low所在的起始叶子;
  2. 循环遍历当前叶子所有 key:
    • key 在[low,high]存入结果;
    • key>high 直接终止遍历返回;
  3. 切换到cur->next下一个叶子,重复遍历。

(四)插入核心:split_child 节点分裂(B + 树分裂规则和 B 树不同)

cpp 复制代码
void split_child(BPlusNode* parent, int idx) {
    BPlusNode* node = parent->children[idx];
    BPlusNode* right = new BPlusNode(node->leaf);
    int mid;
    int up_key;
    if (node->leaf) {
        mid = (node->keys.size() + 1) / 2;
        right->keys.assign(node->keys.begin() + mid, node->keys.end());
        node->keys.resize(mid);
        up_key = right->keys[0];
        link_leaf(node, right);
    } else {
        mid = node->keys.size() / 2;
        up_key = node->keys[mid];
        right->keys.assign(node->keys.begin() + mid + 1, node->keys.end());
        node->keys.resize(mid);
        right->children.assign(node->children.begin() + mid + 1, node->children.end());
        node->children.resize(mid + 1);
    }
    parent->keys.insert(parent->keys.begin() + idx, up_key);
    parent->children.insert(parent->children.begin() + idx + 1, right);
}
函数区分两种节点分裂逻辑:叶子节点 / 索引节点
分支 1:叶子节点分裂
  1. mid = (node->keys.size() + 1) / 2 0 下标整数除法;
  2. 原节点保留前 mid 个 key,新右叶子复制后半段 key;
  3. 提升的 key 是右叶子第一个 key(B 树提升中间 key,这是核心区别);
  4. 调用link_leaf把新右叶子插入双向链表;
  5. 父节点插入提升的分界 key,新增右子节点指针。
分支 2:索引节点分裂(和标准 B 树逻辑一致)
  1. mid = node->keys.size() / 2,取中间 key 上移;
  2. 原节点保留前 mid 个 key 与 mid+1 个子指针;
  3. 右索引节点复制后半段 key 与子指针;
  4. 父节点插入中间分界 key,新增右子节点。
2. void insert(int key) 完整插入流程
cpp 复制代码
void insert(int key) {
    BPlusNode* cur = root;
    vector<pair<BPlusNode*, int>> stack;
    // 向下寻叶匹配规则
    while (!cur->leaf) {
        int i = 0;
        while ((size_t)i < cur->keys.size() && key >= cur->keys[i]) {
            i++;
        }
        stack.emplace_back(cur, i);
        cur = cur->children[i];
    }
    auto it = lower_bound(cur->keys.begin(), cur->keys.end(), key);
    if (it != cur->keys.end() && *it == key) {
        cout << "重复键\n";
        return;
    }
    cur->keys.insert(it, key);

    while ((int)cur->keys.size() > max_keys) {
        if (stack.empty()) {
            BPlusNode* new_root = new BPlusNode(false);
            new_root->children.push_back(cur);
            split_child(new_root, 0);
            root = new_root;
            first_leaf = get_leftmost(root);
            break;
        }
        pair<BPlusNode*, int> item = stack.back();
        stack.pop_back();
        BPlusNode* p = item.first;
        int idx = item.second;
        split_child(p, idx);
        cur = p;
    }
    first_leaf = get_leftmost(root);
}
  1. stack记录向下遍历路径(父节点 + 子树下标),用于向上回溯分裂;
  2. 向下遍历索引层,找到目标叶子节点;
  3. 叶子有序插入 key,重复 key 直接提示并退出;
  4. 循环判断当前节点是否溢出(key 数量 > max_keys):
    • 栈为空:根节点溢出,新建上层索引根,执行分裂,更新全局 root;
    • 栈不为空:取出父节点与下标,调用split_child分裂,切换当前节点为父节点继续校验溢出;
  5. 插入完成后刷新first_leaf链表头。

(五)删除全套函数(叶子 / 索引分开借键、合并)

叶子节点专用操作
cpp 复制代码
void borrow_left_leaf(BPlusNode* parent, int idx) {
    BPlusNode* cur = parent->children[idx];
    BPlusNode* sib = parent->children[idx - 1];
    int val = sib->keys.back();
    sib->keys.pop_back();
    cur->keys.insert(cur->keys.begin(), val);
    parent->keys[idx - 1] = cur->keys[0];
}
void borrow_right_leaf(BPlusNode* parent, int idx) {
    BPlusNode* cur = parent->children[idx];
    BPlusNode* sib = parent->children[idx + 1];
    int val = sib->keys[0];
    sib->keys.erase(sib->keys.begin());
    cur->keys.push_back(val);
    parent->keys[idx] = sib->keys[0];
}
void merge_leaf(BPlusNode* parent, int idx, bool left_side) {
    BPlusNode* cur = parent->children[idx];
    if (left_side) {
        BPlusNode* sib = parent->children[idx - 1];
        sib->keys.insert(sib->keys.end(), cur->keys.begin(), cur->keys.end());
        unlink_leaf(cur);
        parent->keys.erase(parent->keys.begin() + idx - 1);
        parent->children.erase(parent->children.begin() + idx);
    } else {
        BPlusNode* sib = parent->children[idx + 1];
        cur->keys.insert(cur->keys.end(), sib->keys.begin(), sib->keys.end());
        unlink_leaf(sib);
        parent->keys.erase(parent->keys.begin() + idx);
        parent->children.erase(parent->children.begin() + idx + 1);
    }
}
  1. borrow_left_leaf:当前叶子向左邻叶子借 key
    • 左叶子末尾 key 下沉到当前叶子头部;
    • 更新父节点分界 key 为左叶子新末尾值。
  2. borrow_right_leaf:当前叶子向右邻叶子借 key
    • 右叶子首 key 下沉到当前叶子尾部;
    • 更新父节点分界 key 为右叶子新首值。
  3. merge_leaf:两个叶子节点合并,删除其中一个叶子并断开双向链表。
索引节点专用操作
cpp 复制代码
void borrow_left_idx(BPlusNode* parent, int idx) {
    BPlusNode* cur = parent->children[idx];
    BPlusNode* sib = parent->children[idx - 1];
    int sep = parent->keys[idx - 1];
    cur->keys.insert(cur->keys.begin(), sep);
    parent->keys[idx - 1] = sib->keys.back();
    sib->keys.pop_back();
    if (!cur->leaf) {
        cur->children.insert(cur->children.begin(), sib->children.back());
        sib->children.pop_back();
    }
}
void borrow_right_idx(BPlusNode* parent, int idx) {
    BPlusNode* cur = parent->children[idx];
    BPlusNode* sib = parent->children[idx + 1];
    int sep = parent->keys[idx];
    cur->keys.push_back(sep);
    parent->keys[idx] = sib->keys[0];
    sib->keys.erase(sib->keys.begin());
    if (!cur->leaf) {
        cur->children.push_back(sib->children[0]);
        sib->children.erase(sib->children.begin());
    }
}
void merge_idx(BPlusNode* parent, int idx, bool left_side) {
    BPlusNode* cur = parent->children[idx];
    if (left_side) {
        BPlusNode* sib = parent->children[idx - 1];
        sib->keys.push_back(parent->keys[idx - 1]);
        sib->keys.insert(sib->keys.end(), cur->keys.begin(), cur->keys.end());
        sib->children.insert(sib->children.end(), cur->children.begin(), cur->children.end());
        parent->keys.erase(parent->keys.begin() + idx - 1);
        parent->children.erase(parent->children.begin() + idx);
    } else {
        BPlusNode* sib = parent->children[idx + 1];
        cur->keys.push_back(parent->keys[idx]);
        cur->keys.insert(cur->keys.end(), sib->keys.begin(), sib->keys.end());
        cur->children.insert(cur->children.end(), sib->children.begin(), sib->children.end());
        parent->keys.erase(parent->keys.begin() + idx);
        parent->children.erase(parent->children.begin() + idx + 1);
    }
}
  1. borrow_left_idx:索引节点向左兄弟借分界 key + 子树;
  2. borrow_right_idx:索引节点向右兄弟借分界 key + 子树;
  3. merge_idx:两个索引节点合并,吸收父节点分隔 key 与全部子树。
fix_underflow 欠载修复主函数
cpp 复制代码
void fix_underflow(BPlusNode* node, vector<pair<BPlusNode*, int>>& stack) {
    while (node != root && (int)node->keys.size() < min_keys) {
        pair<BPlusNode*, int> item = stack.back();
        stack.pop_back();
        BPlusNode* p = item.first;
        int idx = item.second;

        BPlusNode* left_sib = (idx > 0) ? p->children[idx - 1] : nullptr;
        BPlusNode* right_sib = ((size_t)(idx + 1) < p->children.size()) ? p->children[idx + 1] : nullptr;
        if (node->leaf) {
            if (left_sib && left_sib->leaf && (int)left_sib->keys.size() > min_keys) {
                borrow_left_leaf(p, idx);
                break;
            } else if (right_sib && right_sib->leaf && (int)right_sib->keys.size() > min_keys) {
                borrow_right_leaf(p, idx);
                break;
            } else if (left_sib && left_sib->leaf) {
                merge_leaf(p, idx, true);
            } else if (right_sib && right_sib->leaf) {
                merge_leaf(p, idx, false);
            }
        } else {
            if (left_sib && (int)left_sib->keys.size() > min_keys) {
                borrow_left_idx(p, idx);
                break;
            } else if (right_sib && (int)right_sib->keys.size() > min_keys) {
                borrow_right_idx(p, idx);
                break;
            } else if (left_sib) {
                merge_idx(p, idx, true);
            } else {
                merge_idx(p, idx, false);
            }
        }
        node = p;
    }
    if (root->keys.empty() && !root->children.empty()) {
        root = root->children[0];
        root->prev = root->next = nullptr;
        first_leaf = get_leftmost(root);
    }
}

删除后节点 key 数量低于min_keys时自下而上修复,区分叶子 / 索引两套逻辑:

  1. 循环条件:当前节点不是根、关键字不足下限;
  2. 取出父节点与当前子树下标,判断左右兄弟:
    • 叶子节点:优先向左右叶子借 key;无富余则合并叶子;
    • 索引节点:优先向左右索引兄弟借 key;无富余则合并索引节点;
  3. 修复完成跳出循环,否则将父节点设为当前节点,继续向上修复;
  4. 特殊处理根节点:根无 key 但存在子节点,将第一个子节点设为新根,刷新链表头。
void del(int key) 删除主流程
cpp 复制代码
void del(int key) {
    vector<pair<BPlusNode*, int>> stack;
    BPlusNode* cur = root;
    while (!cur->leaf) {
        int i = 0;
        while ((size_t)i < cur->keys.size() && key >= cur->keys[i]) {
            i++;
        }
        stack.emplace_back(cur, i);
        cur = cur->children[i];
    }
    auto it = lower_bound(cur->keys.begin(), cur->keys.end(), key);
    if (it == cur->keys.end() || *it != key) {
        cout << "不存在\n";
        return;
    }
    cur->keys.erase(it);
    fix_underflow(cur, stack);
    first_leaf = get_leftmost(root);
}
  1. 栈记录遍历路径,向下找到 key 所在叶子;
  2. 叶子内删除目标 key;
  3. 调用fix_underflow自下而上修复节点欠载、借键、合并;
  4. 刷新叶子链表头first_leaf

(六)四大遍历函数(两套中序,区分教学 / 业务场景)

1. traversal_in_business() 业务中序(真实数据,数据库场景)
cpp 复制代码
vector<int> traversal_in_business() {
    vector<int> res;
    BPlusNode* p = first_leaf;
    while (p) {
        res.insert(res.end(), p->keys.begin(), p->keys.end());
        p = p->next;
    }
    return res;
}

B + 树独有,直接遍历叶子双向链表,输出全部真实数据,无索引冗余 key:

cpp 复制代码
BPlusNode* p = first_leaf;
while (p) {
    res.insert(res.end(), p->keys.begin(), p->keys.end());
    p = p->next;
}

对应 MySQL 全表扫描、范围查询的底层遍历逻辑。

2. traversal_in_teaching() DFS 教学中序(包含索引分界 key)
cpp 复制代码
void inorder_dfs(BPlusNode* n, vector<int>& res) {
    if (!n) return;
    if (n->leaf) {
        res.insert(res.end(), n->keys.begin(), n->keys.end());
        return;
    }
    for (int i = 0; (size_t)i < n->keys.size(); i++) {
        inorder_dfs(n->children[i], res);
        res.push_back(n->keys[i]);
    }
    inorder_dfs(n->children.back(), res);
}
vector<int> traversal_in_teaching() {
    vector<int> r;
    inorder_dfs(root, r);
    return r;
}

递归深度优先遍历整棵树,索引节点 key 也会输出,仅用于课堂画图、算法演示,不对应真实数据库业务。

3. traversal_pre() 前序遍历
cpp 复制代码
void preorder(BPlusNode* n, vector<int>& res) {
    if (!n) return;
    res.insert(res.end(), n->keys.begin(), n->keys.end());
    for (auto c : n->children) preorder(c, res);
}
vector<int> traversal_pre() {
    vector<int> r;
    preorder(root, r);
    return r;
}

先输出当前节点全部 key,再依次递归所有子节点。

4. traversal_post() 后序遍历
cpp 复制代码
void postorder(BPlusNode* n, vector<int>& res) {
    if (!n) return;
    for (auto 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;
}

先递归全部子节点,再输出当前节点全部 key。

5. traversal_level() 层序遍历
cpp 复制代码
vector<int> traversal_level() {
    vector<int> r;
    queue<BPlusNode*> q;
    q.push(root);
    while (!q.empty()) {
        auto u = q.front();
        q.pop();
        r.insert(r.end(), u->keys.begin(), u->keys.end());
        for (auto c : u->children) q.push(c);
    }
    return r;
}

队列广度优先,从上到下、每层从左到右输出节点关键字,用于直观打印树分层结构。

6. range_query 区间遍历
cpp 复制代码
vector<int> range_query(int low, int high) {
    vector<int> res;
    BPlusNode* cur = root;
    while (!cur->leaf) {
        int i = 0;
        while ((size_t)i < cur->keys.size() && low >= cur->keys[i]) {
            i++;
        }
        cur = cur->children[i];
    }
    while (cur) {
        for (int k : cur->keys) {
            if (k > high) return res;
            if (k >= low) res.push_back(k);
        }
        cur = cur->next;
    }
    return res;
}

利用叶子链表连续读取,是 B + 树相比 B 树性能碾压的核心函数。

(七)C++完整代码参考地址与运行结果完整展示

1.参考地址

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

GitHub:https://github.com/hongyuxu0/B-Tree-Visualizer/blob/main/bplustree.cpp

2.运行结果完整展示

四、B + 树为什么是数据库标准索引?四大核心优势

1. 范围查询、分页、全表扫描性能碾压 B 树

叶子双向有序链表,区间查询只需顺序遍历链表,磁盘连续 IO;B 树需要频繁切换子树,随机 IO 多。

2. 树高度更低,磁盘 IO 更少

非叶子节点只存索引 key,一页磁盘可以存放更多分界值,同等数据量下树高度远小于 B 树;百万级数据仅 2~3 次磁盘读取。

3. 数据集中存储,缓存友好

所有真实数据集中在最下层叶子,数据库可缓存上层全部索引页,绝大多数等值查询只需要 1 次叶子磁盘 IO。

4. 增删改维护成本更低

上层索引存在冗余 key,删除叶子数据不需要立刻同步修改上层索引,减少多层节点更新操作。

五、完整实操示例:4 阶 Base-0 B+ 树 插入全过程

初始空树,依次插入:5,15,25,35,45,10,8

步骤 1:初始空树,依次插入 5、15、25

初始根为叶子节点,连续插入 3 个 key,未达到上限 3,无分裂

叶子节点:[5, 15, 25]

链表:[5,15,25](唯一叶子,prev/next=nullptr)

树结构:

步骤 2:插入 35,叶子溢出,执行叶子分裂

原叶子 key 数组:[5,15,25,35],size=4

1.叶子分裂计算 mid=4/2=2(0 下标)

  • 左叶子保留下标 0、1:[5, 15]
  • 右叶子存放下标 2、3:[25, 35]

2.提升右叶子第一个 key 25 到父索引;

3.双向链表链接:左叶子 next 指向右叶子,右叶子 prev 指向左叶子;

4.新建非叶子根节点,存放分界 key [25],两个子指针分别指向左、右叶子。

当前完整树结构

叶子链表顺序:[5,15] <-> [25,35]

步骤 3:插入 45,写入右侧叶子

右叶子当前 [25,35],插入 45 后变为 [25,35,45],刚好 3 个 key,未溢出,无需分裂。 树结构不变,仅修改右叶子:

叶子链表:[5,15] <-> [25,35,45]

步骤 4:插入 10,写入左侧叶子

左叶子 [5,15] 插入 10,有序重排为 [5, 10, 15],刚好 3 个 key,无溢出。

叶子链表:[5,10,15] <-> [25,35,45]

步骤 5:插入 8,左侧叶子溢出,叶子分裂

左叶子原有 [5,10,15],插入 8 后数组变为 [5, 8, 10, 15],size=4,触发叶子分裂

1.叶子分裂 mid=4/2=2

  • 左子叶子:下标 0、1 → [5, 8]
  • 右子叶子:下标 2、3 → [10, 15]

2.提升右子叶子首 key 10,插入上层父索引节点;

3.链表更新:原左叶子断开,新两个叶子串联,整体链表:[5,8] <-> [10,15] <-> [25,35,45]

4.父索引原有 [25],插入分界 key 10,索引变为 [10, 25],新增中间子指针指向 [10,15]

最终完整 4 阶 B + 树结构(全部数字插入完成)

叶子链表完整串联:5,810,1525,35,45

六、高频面试核心考点

  1. MySQL 为什么不用 B 树,选择 B + 树? ① 范围查询、分页、全表遍历效率极高;② 树高度更低,IO 更少;③ 索引层体积小,内存缓存友好;④ 增删维护开销更低。
  2. B + 树查找一定走到叶子吗? 是,上层只有索引分界,真实数据仅存叶子,哪怕 key 在索引层匹配,也要下探叶子取数据。
  3. B + 树叶子链表作用? 支持高效区间查询、分页、统计 count、全表扫描。
  4. B + 树删除时上层索引 key 不删除会不会出错? 不会,索引仅做路由标记,即使 key 对应数据已删除,只会导向空叶子,查询逻辑不受影响。
  5. 复合索引(联合主键)底层依旧是 B + 树,叶子 key 存储多字段组合有序值。

七、总结

B+树是一种优化的多路平衡查找树,专为数据库索引设计。其核心特点包括:所有数据仅存储在叶子节点,非叶子节点仅包含索引;叶子节点通过双向链表连接,支持高效范围查询;通过分裂和合并操作保持平衡。相比B树,B+树在范围查询、全表扫描和磁盘IO效率上具有显著优势,因此成为MySQL等数据库的标准索引结构。本文详细解析了B+树的概念、结构、操作原理及C++实现,并对比了其与B树的性能差异,突出了B+树在数据库应用中的四大核心优势。

相关推荐
吃着火锅x唱着歌14 小时前
Effective C++ 学习笔记 条款37 绝不重新定义继承而来的缺省参数值
c++·笔记·学习
小保CPP15 小时前
OCR C++ Tesseract按行识别字符
c++·人工智能·ocr·模式识别·光学字符识别
阿米亚波15 小时前
【C++】流式数据输入处理(不完全整理)
开发语言·c++·笔记
Hardworking66616 小时前
第7章 软硬件系统集成
数据库·软硬件系统集成
名字还没想好☜17 小时前
Go 的 time.Ticker 陷阱:定时任务里被忽略的内存泄漏与正确关闭
java·数据库·golang·go·定时器
皓月斯语17 小时前
【C++基础】三目运算符
开发语言·数据结构·c++
熊猫钓鱼>_>17 小时前
Redis 突发缓存穿透:一次完整的定位复盘
数据库·人工智能·redis·缓存·ai·agent·智能
初願致夕霞17 小时前
C++懒汉单例设计详解
服务器·开发语言·c++
脱胎换骨-军哥17 小时前
C++分布式系统设计:从通信引擎到分布式共识
开发语言·c++·分布式