【B树与B+树详解】

文章目录

  • 前言
  • [一、B 树是什么?](#一、B 树是什么?)
    • [1. B 树节点结构](#1. B 树节点结构)
    • [2. B 树搜索](#2. B 树搜索)
    • [3. B 树插入](#3. B 树插入)
    • [4. B 树删除](#4. B 树删除)
  • [二、B 树 C++ 实现](#二、B 树 C++ 实现)
    • [1. 定义节点类 BTreeNode](#1. 定义节点类 BTreeNode)
      • [2. 定义 BTree 类包装](#2. 定义 BTree 类包装)
    • [3. 代码解析](#3. 代码解析)
    • [4. B 树插入与遍历](#4. B 树插入与遍历)
  • [三、B+ 树是什么?](#三、B+ 树是什么?)
    • [1. B+ 树节点结构](#1. B+ 树节点结构)
    • [2. B+ 树查找](#2. B+ 树查找)
    • [3. B+ 树插入](#3. B+ 树插入)
    • [4. B+ 树删除](#4. B+ 树删除)
  • [四、B+ 树 C++ 实现](#四、B+ 树 C++ 实现)
    • [1. 定义节点基类与派生类](#1. 定义节点基类与派生类)
    • [2. B+ 树类框架](#2. B+ 树类框架)
      • [3. 代码解析与注意点](#3. 代码解析与注意点)
    • [4. B+ 树插入与遍历](#4. B+ 树插入与遍历)
  • [五、B 树与 B+ 树对比与应用场景](#五、B 树与 B+ 树对比与应用场景)
    • [1. 存储结构差异](#1. 存储结构差异)
    • [2. 节点扇出与树高](#2. 节点扇出与树高)
    • [3. 查询效率](#3. 查询效率)
    • [4. 实现与维护复杂度](#4. 实现与维护复杂度)
    • [5. 应用场景](#5. 应用场景)
  • 六、小结

前言

B 树(B-Tree)和 B+ 树(B+Tree)作为磁盘友好型的平衡多路搜索树,在数据库索引、文件系统等场景中得到广泛应用。


一、B 树是什么?

B 树(Balanced Tree 或 B-Tree)是一种平衡的多路搜索树,专门为减少磁盘 I/O 设计。其基本特征:

  • 每个节点可以有多个(m个)子节点,m 称为阶(order)。
  • 节点存储一定范围内的关键字(keys),并且这些关键字在节点内有序。
  • 所有叶子节点在同一层,高度平衡。
  • 每个内部节点(非根)至少有 ⌈m/2⌉ 个子节点,最多 m 个子节点;关键字数量 = 子节点数 - 1。
  • 根节点至少有 2 个子节点(如果不是叶子)。
  • 这样设计可使树的高度尽可能低,减少在外存(磁盘)上的读写次数。

1. B 树节点结构

以最常见的"最小度数 t"表示法:

  • 最小度数 t(t ≥ 2)。
  • 每个节点最多可有 2t - 1 个关键字,最多有 2t 个子指针。
  • 每个非根节点至少有 t - 1 个关键字,至少有 t 个子指针;根节点允许少于 t-1 个关键字。
  • 关键字在节点中按升序排列,子指针对应分割的区间。

一个节点通常包含:

  • int nKeys:当前关键字数量。
  • vector<KeyType> keys:长度可达 2t - 1
  • vector<BTreeNode*> children:长度可达 2t
  • bool isLeaf:是否为叶子节点。

2. B 树搜索

在节点中对关键字做二分查找或线性查找:

  1. 在当前节点的 keys[0...nKeys-1] 中找到第一个 ≥ target 的位置 i。
  2. 若 keys[i] == target,则找到;若当前节点是叶子但不等,则不存在;否则递归到 children[i](若 keys[i] > target)或 children[nKeys](target > 所有 keys)。
  3. 复杂度:O(t) 单节点查找(通常用二分可 O(log t)),高度约 O(log_t N),整体 O(log N)。

3. B 树插入

为了在保持平衡的前提下插入新关键字 k,需要:

  • 如果根节点已满(nKeys == 2t - 1),需先分裂根:创建新根节点,原根成为其第一个子节点,然后对其第一个子节点进行分裂,使得新根至少有两个子节点,此时树高 +1。再从新根递归插入。
  • 插入操作在非满节点进行:从根开始向下定位应插入的叶子节点。在向下前,若要访问的孩子节点已满,则先在父节点中分裂该孩子节点,调整父节点关键字,使孩子"不再满"。这样保证递归到叶子时所在节点必定未满,可直接插入不破坏属性。
  • 分裂过程:对满节点 y(有 2t-1 个关键字),中间第 t-1 位置的 key 上升到父节点;y 左侧 t-1 keys 保留在原节点(或新节点);右侧 t-1 keys 拆到新节点 z;若 y 不是叶子,其对应的 children 也要拆分到 z。

伪代码

复制代码
INSERT(k):
  r = root
  if r.nKeys == 2t-1:
    s = new Node(isLeaf=false)
    root = s
    s.children[0] = r
    SPLIT_CHILD(s, 0)
    INSERT_NONFULL(s, k)
  else:
    INSERT_NONFULL(r, k)

INSERT_NONFULL(x, k):
  if x.isLeaf:
    在 x.keys 中找到插入位置 i,并插入 k,nKeys++
  else:
    找到 i 使得 k 应该插入到 children[i]
    if x.children[i].nKeys == 2t-1:
      SPLIT_CHILD(x, i)
      if k > x.keys[i]: i++
    INSERT_NONFULL(x.children[i], k)

SPLIT_CHILD(parent, i):
  y = parent.children[i]
  z = new Node(isLeaf=y.isLeaf)
  z.nKeys = t - 1
  for j in 0..t-2: z.keys[j] = y.keys[j + t]
  if not y.isLeaf:
    for j in 0..t-1: z.children[j] = y.children[j + t]
  y.nKeys = t - 1
  在 parent.children 中插入 z 在位置 i+1,并在 parent.keys 中插入 y.keys[t-1]

4. B 树删除

删除较为复杂,涉及以下几种情况:

  • 从叶子节点删除:若关键字在叶子节点且该叶子节点删除后仍满足最小关键字数,则直接删除;若删除后导致关键字数 < t-1,需要通过以下操作修复:

    • 从相邻兄弟节点借一个关键字(借时要调整父节点关键字);或
    • 与相邻兄弟节点合并,再将父节点相应关键字下移到合并节点;可能递归向上修复。
  • 从内部节点删除:若关键字在内部节点 x.keys[i]:

    • 若前驱子树(children[i])至少有 t 个关键字,可找到前驱 key 替换 x.keys[i],然后在 children[i] 递归删除前驱 key;
    • 否若后继子树(children[i+1])至少有 t 个关键字,可找到后继 key 替换 x.keys[i],再在 children[i+1] 递归删除后继 key;
    • 否两侧子树都只有 t-1 个关键字,则将 x.keys[i] 与 children[i+1] 合并到 children[i],然后在合并后的节点中递归删除 k。
  • 关键在于在向下递归时确保访问的节点满足至少 t 个关键字(非根),否则先做借或合并操作,使其拥有足够关键字再递归。

二、B 树 C++ 实现

1. 定义节点类 BTreeNode

cpp 复制代码
#include <vector>
#include <iostream>
#include <algorithm>

template<typename KeyType>
class BTreeNode {
public:
    bool isLeaf;
    int nKeys;
    std::vector<KeyType> keys;
    std::vector<BTreeNode*> children;
    int t;  // 最小度数

    BTreeNode(int _t, bool _isLeaf)
        : isLeaf(_isLeaf), nKeys(0), t(_t) {
        // keys 最多 2t-1, children 最多 2t
        keys.reserve(2 * t - 1);
        children.reserve(2 * t);
    }

    // 查找 key 在节点中的索引或应插入位置
    int findKey(const KeyType& k) {
        int idx = 0;
        // 线性查找也可换二分:std::lower_bound
        while (idx < nKeys && keys[idx] < k)
            ++idx;
        return idx;
    }

    // 插入到非满节点
    void insertNonFull(const KeyType& k) {
        int i = nKeys - 1;
        if (isLeaf) {
            // 在叶子节点中插入:找到位置并插入
            keys.push_back(KeyType());  // 扩容占位
            while (i >= 0 && keys[i] > k) {
                keys[i + 1] = keys[i];
                --i;
            }
            keys[i + 1] = k;
            nKeys++;
        } else {
            // 内部节点:找到子节点 index
            int idx = findKey(k);
            // 若孩子已满,需要先分裂
            if (children[idx]->nKeys == 2 * t - 1) {
                splitChild(idx);
                // 分裂后看是否 k 应该落在右侧新节点
                if (keys[idx] < k)
                    idx++;
            }
            children[idx]->insertNonFull(k);
        }
    }

    // 分裂 children[idx]
    void splitChild(int idx) {
        BTreeNode* y = children[idx];
        BTreeNode* z = new BTreeNode(y->t, y->isLeaf);
        z->nKeys = t - 1;

        // 将 y.keys[t..2t-2] 移至 z
        for (int j = 0; j < t - 1; ++j)
            z->keys.push_back(y->keys[j + t]);
        // 如果 y 非叶子,移动子指针
        if (!y->isLeaf) {
            for (int j = 0; j < t; ++j)
                z->children.push_back(y->children[j + t]);
        }
        y->nKeys = t - 1;

        // 在 children 中插入 z
        children.insert(children.begin() + idx + 1, z);
        // 在 keys 中插入 y->keys[t-1]
        keys.insert(keys.begin() + idx, y->keys[t - 1]);
        nKeys++;

        // 清理 y 多余的 keys/children(可选,为简化例子不释放底层内存)
        y->keys.resize(t - 1);
        if (!y->isLeaf)
            y->children.resize(t);
    }

    // 遍历(调试用)
    void traverse(int depth = 0) {
        // 缩进
        for (int i = 0; i < depth; ++i) std::cout << "  ";
        std::cout << "[";
        for (int i = 0; i < nKeys; ++i) {
            std::cout << keys[i];
            if (i + 1 < nKeys) std::cout << ", ";
        }
        std::cout << "]";
        std::cout << (isLeaf ? " (leaf)" : "") << "\n";
        if (!isLeaf) {
            for (int i = 0; i <= nKeys; ++i) {
                children[i]->traverse(depth + 1);
            }
        }
    }

    // 搜索 key
    BTreeNode* search(const KeyType& k) {
        int i = findKey(k);
        if (i < nKeys && keys[i] == k)
            return this;
        if (isLeaf)
            return nullptr;
        return children[i]->search(k);
    }

    // 删除 key 的公共接口
    void remove(const KeyType& k) {
        int idx = findKey(k);
        if (idx < nKeys && keys[idx] == k) {
            // key 在此节点
            if (isLeaf) {
                // 叶子节点直接删除
                keys.erase(keys.begin() + idx);
                nKeys--;
            } else {
                removeFromNonLeaf(idx);
            }
        } else {
            // key 不在此节点
            if (isLeaf) {
                // 不存在
                std::cout << "Key " << k << " does not exist in the tree\n";
                return;
            }
            // 决定进入子节点 children[idx]
            bool flag = (idx == nKeys);
            // 如果 children[idx] 关键字数不足,需要先填充
            if (children[idx]->nKeys < t) {
                fill(idx);
            }
            // 如果最初 idx==nKeys,并且 fill 导致合并后 children[idx-1],则递归在 children[idx-1]
            if (flag && idx > nKeys)
                children[idx - 1]->remove(k);
            else
                children[idx]->remove(k);
        }
    }

    // 从非叶节点删除 keys[idx]
    void removeFromNonLeaf(int idx) {
        KeyType k = keys[idx];
        // 前驱子树 children[idx]
        if (children[idx]->nKeys >= t) {
            KeyType pred = getPredecessor(idx);
            keys[idx] = pred;
            children[idx]->remove(pred);
        }
        // 后继子树 children[idx+1]
        else if (children[idx + 1]->nKeys >= t) {
            KeyType succ = getSuccessor(idx);
            keys[idx] = succ;
            children[idx + 1]->remove(succ);
        }
        else {
            // 两侧子节点都只有 t-1 个 key,合并 idx 和 idx+1
            merge(idx);
            children[idx]->remove(k);
        }
    }

    KeyType getPredecessor(int idx) {
        BTreeNode* cur = children[idx];
        while (!cur->isLeaf)
            cur = cur->children[cur->nKeys];
        return cur->keys[cur->nKeys - 1];
    }

    KeyType getSuccessor(int idx) {
        BTreeNode* cur = children[idx + 1];
        while (!cur->isLeaf)
            cur = cur->children[0];
        return cur->keys[0];
    }

    // fill children[idx] 使其至少有 t 个关键字
    void fill(int idx) {
        // 如果前兄弟有多余,借
        if (idx > 0 && children[idx - 1]->nKeys >= t) {
            borrowFromPrev(idx);
        }
        // 后兄弟有多余,借
        else if (idx < nKeys && children[idx + 1]->nKeys >= t) {
            borrowFromNext(idx);
        }
        else {
            // 合并
            if (idx < nKeys)
                merge(idx);
            else
                merge(idx - 1);
        }
    }

    void borrowFromPrev(int idx) {
        BTreeNode* child = children[idx];
        BTreeNode* sibling = children[idx - 1];
        // child 的 keys 后移,腾出位置
        child->keys.insert(child->keys.begin(), keys[idx - 1]);
        if (!child->isLeaf) {
            child->children.insert(child->children.begin(), sibling->children.back());
            sibling->children.pop_back();
        }
        // 把 sibling 最后一个 key 上移到父节点
        keys[idx - 1] = sibling->keys.back();
        sibling->keys.pop_back();
        sibling->nKeys--;
        child->nKeys++;
    }

    void borrowFromNext(int idx) {
        BTreeNode* child = children[idx];
        BTreeNode* sibling = children[idx + 1];
        // 把父节点 keys[idx] 下移到 child
        child->keys.push_back(keys[idx]);
        if (!child->isLeaf) {
            child->children.push_back(sibling->children.front());
            sibling->children.erase(sibling->children.begin());
        }
        // sibling 第一个 key 上移到父节点
        keys[idx] = sibling->keys.front();
        sibling->keys.erase(sibling->keys.begin());
        sibling->nKeys--;
        child->nKeys++;
    }

    // 合并 children[idx] 和 children[idx+1]
    void merge(int idx) {
        BTreeNode* child = children[idx];
        BTreeNode* sibling = children[idx + 1];
        // 把父节点 keys[idx] 下移到 child
        child->keys.push_back(keys[idx]);
        // 将 sibling 的 keys 和 children 追加到 child
        for (int i = 0; i < sibling->nKeys; ++i)
            child->keys.push_back(sibling->keys[i]);
        if (!child->isLeaf) {
            for (int i = 0; i <= sibling->nKeys; ++i)
                child->children.push_back(sibling->children[i]);
        }
        // 更新 child 关键字数
        child->nKeys += sibling->nKeys + 1;
        // 从父节点移除 keys[idx] 和 children[idx+1]
        keys.erase(keys.begin() + idx);
        children.erase(children.begin() + idx + 1);
        nKeys--;
        // 释放 sibling(可选)
        delete sibling;
    }
};

2. 定义 BTree 类包装

cpp 复制代码
template<typename KeyType>
class BTree {
public:
    BTreeNode<KeyType>* root;
    int t;

    BTree(int _t) : root(nullptr), t(_t) {}

    // 遍历
    void traverse() {
        if (root) root->traverse();
        else std::cout << "Empty tree\n";
    }

    // 搜索
    BTreeNode<KeyType>* search(const KeyType& k) {
        return root ? root->search(k) : nullptr;
    }

    // 插入
    void insert(const KeyType& k) {
        if (!root) {
            root = new BTreeNode<KeyType>(t, true);
            root->keys.push_back(k);
            root->nKeys = 1;
        } else {
            if (root->nKeys == 2 * t - 1) {
                BTreeNode<KeyType>* s = new BTreeNode<KeyType>(t, false);
                s->children.push_back(root);
                s->splitChild(0);
                // 新根 s 有两个子节点
                int i = 0;
                if (s->keys[0] < k) i++;
                s->children[i]->insertNonFull(k);
                root = s;
            } else {
                root->insertNonFull(k);
            }
        }
    }

    // 删除
    void remove(const KeyType& k) {
        if (!root) {
            std::cout << "Empty tree\n";
            return;
        }
        root->remove(k);
        if (root->nKeys == 0) {
            BTreeNode<KeyType>* tmp = root;
            if (root->isLeaf) {
                delete root;
                root = nullptr;
            } else {
                root = root->children[0];
                delete tmp;
            }
        }
    }
};

3. 代码解析

  • 内存管理:上述示例中较为简化,没有做完备的内存管理(如删除所有节点时的递归释放)。在真实项目中,需要在析构函数中遍历所有节点并释放内存,或使用智能指针改写。
  • 常量 t 选取:在内存实现中,t 决定节点能存储 key 数量。若 t 较大,单节点开销也变大;但在磁盘/页面场景,t 取决于页大小与 key 大小,尽量让节点填满一页以减少 I/O。内存示例中一般取较小值(如 t=3 或 4)做演示。
  • 查找优化 :节点内部查找用线性或二分查找。示例用线性,若 keys 数较大可改成 std::lower_bound(keys.begin(), keys.end(), k)
  • 调试traverse 方法用于打印树结构,可插入若干测试打印,帮助理解插入/删除后树的变化。
  • 错误处理:示例中删除时若不存在直接输出提示;生产环境中可改为抛异常或返回状态。

4. B 树插入与遍历

cpp 复制代码
int main() {
    int t = 3; // 最小度数
    BTree<int> tree(t);

    int keysToInsert[] = {10, 20, 5, 6, 12, 30, 7, 17};
    for (int k : keysToInsert) {
        std::cout << "Insert " << k << ":\n";
        tree.insert(k);
        tree.traverse();
        std::cout << "-------------------\n";
    }

    int searchKey = 6;
    auto node = tree.search(searchKey);
    if (node)
        std::cout << "Found key " << searchKey << " in a node.\n";
    else
        std::cout << "Key " << searchKey << " not found.\n";

    // 删除示例
    tree.remove(6);
    std::cout << "After removing 6:\n";
    tree.traverse();

    return 0;
}

三、B+ 树是什么?

B+ 树是在 B 树基础上的一种变体,更加适合范围查询与磁盘顺序访问。其核心差异与特征如下:

  1. 只将关键字存储在内部节点:内部节点仅保存用于分割子树的关键字,不存数据记录指针(或只存最小/最大 key 用于导航)。所有实际数据(或数据指针)只保存在叶子节点。
  2. 叶子节点链表:所有叶子节点通过指针双向或单向串联,便于范围查询时的顺序遍历。
  3. 内部节点仅做索引作用:插入/删除时保持内部节点也按 B 树规则分裂/合并,但不会在内部节点存储完整数据,只存导航 key。
  4. 更高的扇出:由于内部节点只存 key,不存数据指针对应较小,可使每个节点容纳更多子节点,降低树高。

典型应用场景:数据库聚簇索引或非聚簇索引。范围查询、排序扫描时,只需在叶节点链表上顺序遍历,无需回到父节点。

1. B+ 树节点结构

可用最小度数 t 表示(略有不同社区里用阶 order 表示):

  • 内部节点:

    • 最多 2t 个子指针,最多 2t - 1 个 key(用于区分子树范围)。
    • 最少子指针数:t(除根),最少 key 是子指针数-1。
  • 叶子节点:

    • 存储实际数据指针或记录;可存 L 个 records,最少 ⌈L/2⌉(除根或特殊情况)。
    • 叶子节点包含 key(或 key+value)数组,并有指向下一个叶子节点的指针 next(也可双向)。
    • 通常叶节点也要保存子指针或记录指针,内部节点不保存这些数据指针。

2. B+ 树查找

查找 key:

  • 从根开始,在内部节点中找到合适子指针向下,直至叶子节点,然后在叶子节点 keys 中查找是否存在。
  • 复杂度 O(log N)。

3. B+ 树插入

  • 定位到叶子节点,若叶子未满,直接插入并保持 keys 有序。
  • 若叶子已满,分裂叶子:将叶子节点拆为两个,通常将中间或上半部分移到新叶子,调整父节点插入新的分割 key(通常是新叶首 key);若父节点满则递归分裂。
  • 分裂可能向上递归至根,必要时生成新根,树高 +1。
  • 内部节点只保存用于导航的 key,不保存值。

4. B+ 树删除

  • 在叶子节点删除 key,若删除后叶子节点关键字数不足:

    • 从相邻叶兄弟借 key(同时更新父导航 key);
    • 或与兄弟合并,并更新父节点;可能递归向上。
  • 内部节点若不再有足够子指针,也做借或合并。

  • 注意:删除时必须维护叶子链表连通性。

四、B+ 树 C++ 实现

1. 定义节点基类与派生类

cpp 复制代码
#include <vector>
#include <iostream>
#include <algorithm>

// 简化:这里我们假设叶子节点存储 key(如有 value,可改为 pair)
template<typename KeyType>
class BPlusNode {
public:
    bool isLeaf;
    std::vector<KeyType> keys;
    BPlusNode* parent;

    BPlusNode(bool leaf) : isLeaf(leaf), parent(nullptr) {}
    virtual ~BPlusNode() = default;
};

template<typename KeyType>
class BPlusInternalNode : public BPlusNode<KeyType> {
public:
    // children.size() = keys.size() + 1
    std::vector<BPlusNode<KeyType>*> children;

    BPlusInternalNode() : BPlusNode<KeyType>(false) {}

    // 在索引中查找子节点下标
    int findChildIndex(const KeyType& k) {
        int idx = 0;
        while (idx < (int)this->keys.size() && k >= this->keys[idx])
            ++idx;
        return idx;
    }
};

template<typename KeyType>
class BPlusLeafNode : public BPlusNode<KeyType> {
public:
    BPlusLeafNode* next;  // 叶子链表指针
    BPlusLeafNode* prev;
    // 存储 keys; 若需要存储 value,可改为 vector<pair<KeyType, ValueType>>
    std::vector<KeyType> values;

    BPlusLeafNode() : BPlusNode<KeyType>(true), next(nullptr), prev(nullptr) {}
};

2. B+ 树类框架

cpp 复制代码
template<typename KeyType>
class BPlusTree {
public:
    BPlusNode<KeyType>* root;
    int t; // 最小度数或阶,根据需要定义;在内部节点最多 2t 子指针,叶子最多 2t keys

    BPlusTree(int _t) : root(nullptr), t(_t) {}

    // 搜索
    BPlusLeafNode<KeyType>* search(const KeyType& k) {
        if (!root) return nullptr;
        BPlusNode<KeyType>* cur = root;
        // 向下查找到叶子
        while (!cur->isLeaf) {
            auto inode = static_cast<BPlusInternalNode<KeyType>*>(cur);
            int idx = inode->findChildIndex(k);
            cur = inode->children[idx];
        }
        auto leaf = static_cast<BPlusLeafNode<KeyType>*>(cur);
        // 在 leaf->values 中查找
        auto it = std::lower_bound(leaf->values.begin(), leaf->values.end(), k);
        if (it != leaf->values.end() && *it == k)
            return leaf;
        return nullptr;
    }

    // 插入
    void insert(const KeyType& k) {
        if (!root) {
            auto leaf = new BPlusLeafNode<KeyType>();
            leaf->values.push_back(k);
            root = leaf;
            return;
        }
        // 找到要插入的叶子
        BPlusLeafNode<KeyType>* leaf = findLeafNode(k);
        insertIntoLeaf(leaf, k);
    }

private:
    BPlusLeafNode<KeyType>* findLeafNode(const KeyType& k) {
        BPlusNode<KeyType>* cur = root;
        while (!cur->isLeaf) {
            auto inode = static_cast<BPlusInternalNode<KeyType>*>(cur);
            int idx = inode->findChildIndex(k);
            cur = inode->children[idx];
        }
        return static_cast<BPlusLeafNode<KeyType>*>(cur);
    }

    void insertIntoLeaf(BPlusLeafNode<KeyType>* leaf, const KeyType& k) {
        // 插入并保持有序
        auto it = std::lower_bound(leaf->values.begin(), leaf->values.end(), k);
        leaf->values.insert(it, k);
        // 如果超出上限 2t,需分裂
        if ((int)leaf->values.size() > 2 * t) {
            splitLeaf(leaf);
        }
    }

    void splitLeaf(BPlusLeafNode<KeyType>* leaf) {
        int total = leaf->values.size();
        int mid = total / 2;
        // 创建新叶子
        auto newLeaf = new BPlusLeafNode<KeyType>();
        // 右半部分移入 newLeaf
        newLeaf->values.assign(leaf->values.begin() + mid, leaf->values.end());
        leaf->values.resize(mid);

        // 插入到链表
        newLeaf->next = leaf->next;
        if (leaf->next) leaf->next->prev = newLeaf;
        leaf->next = newLeaf;
        newLeaf->prev = leaf;

        // 设置 parent
        newLeaf->parent = leaf->parent;

        // 把 newLeaf 的首 key 提升到父节点
        KeyType upKey = newLeaf->values.front();
        insertIntoParent(leaf, upKey, newLeaf);
    }

    void insertIntoParent(BPlusNode<KeyType>* leftNode, const KeyType& key, BPlusNode<KeyType>* rightNode) {
        if (!leftNode->parent) {
            // 创建新根
            auto newRoot = new BPlusInternalNode<KeyType>();
            newRoot->keys.push_back(key);
            newRoot->children.push_back(leftNode);
            newRoot->children.push_back(rightNode);
            leftNode->parent = newRoot;
            rightNode->parent = newRoot;
            root = newRoot;
            return;
        }
        auto parent = static_cast<BPlusInternalNode<KeyType>*>(leftNode->parent);
        // 在 parent->keys 中找到插入位置
        auto itKey = std::upper_bound(parent->keys.begin(), parent->keys.end(), key);
        int idx = itKey - parent->keys.begin();
        parent->keys.insert(itKey, key);
        parent->children.insert(parent->children.begin() + idx + 1, rightNode);
        rightNode->parent = parent;
        // 如果 parent 超过子指针上限 2t+1?
        if ((int)parent->children.size() > 2 * t + 1) {
            splitInternal(parent);
        }
    }

    void splitInternal(BPlusInternalNode<KeyType>* inode) {
        int totalChildren = inode->children.size();
        int midIndex = totalChildren / 2; // e.g., children: 0..midIndex-1 | midIndex | midIndex+1..
        KeyType upKey = inode->keys[midIndex - 1]; 
        // 创建新内部节点
        auto newInternal = new BPlusInternalNode<KeyType>();
        // 右半部分 children 和 keys 移动到 newInternal
        // children 从 midIndex 开始移
        newInternal->children.assign(inode->children.begin() + midIndex, inode->children.end());
        // keys 从 midIndex 开始移(keys 数 = children.size()-1)
        newInternal->keys.assign(inode->keys.begin() + midIndex, inode->keys.end());

        // 更新 parent 指针
        for (auto child : newInternal->children) {
            child->parent = newInternal;
        }

        // 剪裁原 inode
        inode->children.resize(midIndex);
        inode->keys.resize(midIndex - 1);

        // 将 upKey 插入到父节点
        newInternal->parent = inode->parent;
        insertIntoParent(inode, upKey, newInternal);
    }

public:
    // 遍历叶子链表,调试用
    void traverseLeaves() {
        // 找到最左叶
        BPlusNode<KeyType>* cur = root;
        if (!cur) {
            std::cout << "Empty B+ tree\n";
            return;
        }
        while (!cur->isLeaf) {
            cur = static_cast<BPlusInternalNode<KeyType>*>(cur)->children[0];
        }
        auto leaf = static_cast<BPlusLeafNode<KeyType>*>(cur);
        // 顺序打印所有叶
        while (leaf) {
            std::cout << "[";
            for (auto &k : leaf->values) std::cout << k << " ";
            std::cout << "] -> ";
            leaf = leaf->next;
        }
        std::cout << "NULL\n";
    }
};

3. 代码解析与注意点

  • 最小度数 t :在 B+ 树中,叶子节点最多保存 2t 个 key;内部节点最多保存 2t+1 个子指针(也可设计为最多 2t 子指针,关键看定义)。示例选择叶子超限为 >2t,内部超限为 >2t+1。
  • 提升 key:在分裂叶子时,将新叶首 key 提升到父节点;在分裂内部节点时,将中间 key 提升到更上层。
  • 链表维护:叶子链表使范围查询高效,删除或插入时需维护 prev/next 指针。
  • 内存管理:示例中没有做完整析构,实际需递归释放所有节点;可考虑智能指针或手动写析构。
  • 重复 key:示例允许重复插入同一 key,如需禁止,可在插入前搜索或在插入叶子时检查并跳过。
  • 值存储 :示例叶子只存 key;实际可存 (key, value) 对。只需将 values 改为 vector<pair<KeyType, ValueType>>,并调整比较逻辑。
  • 范围查询示例 :可以在叶子链表上进行 for node = firstLeafWithKey(k1); node && node->values[i] <= k2; node=node->next 打印范围 [k1, k2]。

4. B+ 树插入与遍历

cpp 复制代码
int main() {
    int t = 2; // 最小度数
    BPlusTree<int> bpt(t);
    std::vector<int> keys = {10, 20, 5, 6, 12, 30, 7, 17, 3, 25, 1};
    for (int k : keys) {
        std::cout << "Insert " << k << " into B+ tree\n";
        bpt.insert(k);
        std::cout << "Leaf traversal: ";
        bpt.traverseLeaves();
        std::cout << "-----------------\n";
    }

    int searchKey = 12;
    auto leaf = bpt.search(searchKey);
    if (leaf)
        std::cout << "Found " << searchKey << " in leaf node\n";
    else
        std::cout << searchKey << " not found\n";

    return 0;
}

五、B 树与 B+ 树对比与应用场景

1. 存储结构差异

  • B 树:关键字和数据指针(或记录指针)都存储在节点(内部与叶子)。查找时可在内部节点找到目标并停止,无需到叶子。但范围查询需遍历较麻烦,需要中序遍历整个子树。
  • B+ 树:只有叶子节点存数据;内部节点仅保存导航 key。查找需遍历到叶子节点才能确定存在与否。范围查询高效:一旦到达起始叶节点,可顺序沿叶子链表遍历,不用返回父节点。

2. 节点扇出与树高

  • B+ 树内部节点去掉了数据指针/值存储,只存 key 和子指针,单节点能容纳更多子指针,树高通常更低(更少层次),磁盘 I/O 更少。
  • B 树内部节点存数据指针,容量略低,树高可能略高。

3. 查询效率

  • 单条精确查询:B 树可在内部节点直接找到并返回,无须到叶;B+ 树一般到叶再返回。若内部节点保存完整记录指针,则 B 树略优;但 B+ 树因树高低,差异不大。
  • 范围查询:B+ 树优势明显,可顺序扫描叶链;B 树需中序遍历,较复杂且效率低。

4. 实现与维护复杂度

  • B 树删除逻辑稍复杂,但 B+ 树更复杂些,因要维护叶链和内部导航 key 的更新。
  • B 树节点分裂、合并影响内部和叶节点的数据分布;B+ 树操作则分裂叶和内部略有不同。

5. 应用场景

  • 数据库索引:多数数据库索引(尤其聚簇索引/非聚簇索引)采用 B+ 树,因为范围查询和顺序扫描常见;叶链表支持快速顺序读取。
  • 文件系统:目录索引、文件块索引等可用 B+ 树。
  • 内存结构:若纯内存应用,AVL/红黑树、跳表等更常见;B 树/B+ 树多用于磁盘/大数据场景,或需要高扇出减少指针跳转场景。

六、小结

  • B 树:多路搜索树,内部和叶子节点均保存数据指针;适合精确查询;范围查询需中序遍历。
  • B+ 树:内部节点仅保存导航 key,叶子节点保存所有数据并通过链表连接;范围查询高效,扇出更大,树高更低,适合磁盘/大规模数据场景。
  • 实现思路:关键在分裂、合并、借 key 操作的正确处理,需维护节点 key 数和子指针关系;B+ 树还需维护叶链结构和父节点导航 key 更新。
相关推荐
南莺莺6 小时前
邻接矩阵的基本操作
数据结构·算法··邻接矩阵
观望过往6 小时前
【Java数据结构】队列详解与经典 OJ 题目实战
java·数据结构
aramae7 小时前
详细分析平衡树--红黑树(万字长文/图文详解)
开发语言·数据结构·c++·笔记·算法
CHEN5_027 小时前
【leetcode100】和为k的子数组(两种解法)
java·数据结构·算法
guguhaohao9 小时前
list,咕咕咕!
数据结构·c++·list
Code小翊9 小时前
希尔排序基础理解
数据结构·算法·排序算法
Pluchon10 小时前
硅基计划4.0 算法 二叉树深搜(DFS)
java·数据结构·算法·leetcode·深度优先·剪枝
Yupureki10 小时前
从零开始的C++学习生活 9:stack_queue的入门使用和模板进阶
c语言·数据结构·c++·学习·visual studio
小年糕是糕手11 小时前
【数据结构】单链表“0”基础知识讲解 + 实战演练
c语言·开发语言·数据结构·c++·学习·算法·链表