【B树与B+树详解】
文章目录
- 前言
- 一、B 树是什么?
- 1. B 树节点结构
- 2. B 树搜索
- 3. B 树插入
- 伪代码
- 4. B 树删除
- 二、B 树 C++ 实现
- 1. 定义节点类 BTreeNode
- 2. 定义 BTree 类包装
- 3. 代码解析
- 4. B 树插入与遍历
- 三、B+ 树是什么?
- 1. B+ 树节点结构
- 2. B+ 树查找
- 3. B+ 树插入
- 4. B+ 树删除
- 四、B+ 树 C++ 实现
- 1. 定义节点基类与派生类
- 2. B+ 树类框架
- 3. 代码解析与注意点
- 4. B+ 树插入与遍历
- 五、B 树与 B+ 树对比与应用场景
- 1. 存储结构差异
- 2. 节点扇出与树高
- 3. 查询效率
- 4. 实现与维护复杂度
- 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 = rootif r.nKeys == 2t-1:s = new Node(isLeaf=false)root = ss.children[0] = rSPLIT_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 - 1for 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
#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 最多 2tkeys.reserve(2 * t - 1);children.reserve(2 * t);}// 查找 key 在节点中的索引或应插入位置int findKey(const KeyType& k) {int idx = 0;// 线性查找也可换二分:std::lower_boundwhile (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 {// 内部节点:找到子节点 indexint 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] 移至 zfor (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 中插入 zchildren.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);}}}// 搜索 keyBTreeNode* 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);elsechildren[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+1merge(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);elsemerge(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] 下移到 childchild->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] 下移到 childchild->keys.push_back(keys[idx]);// 将 sibling 的 keys 和 children 追加到 childfor (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 类包装
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 树插入与遍历
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";elsestd::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. 定义节点基类与派生类
#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() + 1std::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+ 树类框架
template<typename KeyType>
class BPlusTree {
public:BPlusNode<KeyType>* root;int t; // 最小度数或阶,根据需要定义;在内部节点最多 2t 子指针,叶子最多 2t keysBPlusTree(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>();// 右半部分移入 newLeafnewLeaf->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;// 设置 parentnewLeaf->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;}// 剪裁原 inodeinode->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+ 树插入与遍历
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";elsestd::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 更新。