文章目录
- 前言
- [一、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 树搜索
在节点中对关键字做二分查找或线性查找:
- 在当前节点的 keys[0...nKeys-1] 中找到第一个 ≥ target 的位置 i。
- 若 keys[i] == target,则找到;若当前节点是叶子但不等,则不存在;否则递归到 children[i](若 keys[i] > target)或 children[nKeys](target > 所有 keys)。
- 复杂度: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 树基础上的一种变体,更加适合范围查询与磁盘顺序访问。其核心差异与特征如下:
- 只将关键字存储在内部节点:内部节点仅保存用于分割子树的关键字,不存数据记录指针(或只存最小/最大 key 用于导航)。所有实际数据(或数据指针)只保存在叶子节点。
- 叶子节点链表:所有叶子节点通过指针双向或单向串联,便于范围查询时的顺序遍历。
- 内部节点仅做索引作用:插入/删除时保持内部节点也按 B 树规则分裂/合并,但不会在内部节点存储完整数据,只存导航 key。
- 更高的扇出:由于内部节点只存 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 更新。