🔥keyipatience:个人主页
🎬作者简介:C/C++后端开发学习者
🌟专栏传送门:《c++》《linux》《c++高阶数据结构》《c++数据结构与算法》
⭐️patience is key in life
初识B树
简单来说B树其实就是一颗平衡多插树,对比我们前面学习的平衡二叉树,多了几个分支
下面以5阶B树为例:

先判断这棵树是5 阶 B 树(m=5) m 阶 B 树:最多
m个分支,m-1个关键字。 看结点23,35,47,55,里面有最多有4 个关键字 → 4 个关键字对应 5 个分支,所以 m=5。
m:阶数 ,k:分支 ,n:关键字 , n=k-1;
任意一颗B数都满足m/2 ≤ k ≤ m,m/2-1<=n<=m-1
在这里即3<=k<=5,2<=n<=4;
B树性质·
平衡:所有叶结点都在同一层
定义:B 树永远平衡,全部叶子结点处于同一高度,不会出现有的叶子很深、有的很浅。
看例子:
- 根:
17(第 0 层) - 中间层:
6 13和23 35 47 55(第 1 层) - 叶子层:
1 4、8 11、14 16、19 22、27 34、38 45、49 53、65 74 79(全部都在第 2 层)
所有叶子都在最下面同一层,满足 B 树平衡特性。
对比二叉搜索树:二叉树最坏会退化成链表,叶子高度参差不齐;B 树强制叶子同层,所以查询稳定。
有序:结点内有序;关键字划分左右子树区间
两点: ① 同一个结点内部的关键字从小到大排好序 ② 任意关键字,左子树全部 <该关键字,右子树全部> 该关键字
看例子
- 结点
23,35,47,55:内部23<35<47<55有序- 23 左子树:
19,22< 23 - 23~35 子树:
27,34,23<27,34<35 - 35~47 子树:
38,45,35<38,45<47 - 47~55 子树:
49,53,47<49,53<55 - 55 右子树:
65,74,79>55
- 23 左子树:
查找逻辑:比如找 38,从根 17 → 大于 17 走右子树
23,35,47,55;38 大于 35,小于 47,进入 35 和 47 之间的子树,找到38,45。
多路:m 阶 B 树,结点分支数有上下限
对于 m 阶 B 树的结点:
- 上限(所有结点通用,根、普通结点一样) 最多:m 个分支,m-1个 个关键字(元素)
关键字数量永远 = 分支数量 − 1
2.下限(重点!根结点和其他结点分开)
- 根结点:最少 2 个分支,1 个关键字
特殊情况:如果整棵树只有根一个节点(没有孩子),根可以0 个分支,1 个关键字
- 其他所有非根结点:最少m/2个分支,m/2-1个关键字
以m=5这棵树为例
- 上限:全部结点最多 5 分支,4 关键字 例子:第二层结点
23,35,47,55,4 个关键字,5 个分支,刚好顶到上限。 - 下限:
- 根结点:最少 2 分支、1 关键字。图里根
17正好:1 个关键字、2 个分支 - 普通非根结点:最少 3 分支、2 关键字。 看叶子结点
1 4:2 个关键字,满足≥2; 中间结点6 13:2 个关键字,满足≥2。
- 根结点:最少 2 分支、1 关键字。图里根
一步步实现B树的构造,插入和中序遍历
1.BTreeNode 结点结构体
template <class K,int M>
struct BTreeNode
{
//多给一个空间,就可以先插入了再分裂
// 存放关键字,最多M个
K _keys[M];
// 子节点指针,最多M个关键字所以开M+1空间
BTreeNode<K, M>* _subs[M+1];
//实际存储多少个关键字
int _n;
// 父节点指针,分裂的时候向上找父节点
BTreeNode<K, M>* _parent;
//初始化
BTreeNode()
:_parent(nullptr)
,_n(0)
{
for (int i = 0; i < M; i++)
{
_keys[i] = K();
_subs[i] = nullptr;
}
_subs[M] = nullptr;
}
};
M 阶 B 树最多只能放 M-1 个关键字 ,_keyM和_subsM+1多预留一个位置是为了插入临时填满之后再分裂
2.BTree 类 + 内部 typedef
typedef BTreeNode<K, M>Node;
private:
Node* _root = nullptr;
3. Find 函数:查找 key,返回 pair <结点指针,下标>
pair<Node*, int>Find(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
int i = 0;
while (cur)
{
// 在当前结点内部顺序查找关键字
while (i < cur->_n)
{
if (key < cur->_keys[i])
{
break; // 目标落在第i个子树
}
else if (key > cur->_keys[i])
{
i++; // 继续往后找
}
else
{
return make_pair(cur, i); // 找到,返回结点+下标
}
}
// 进入子树继续查找
parent = cur;
cur = cur->_subs[i];// 去subs[i]子树;
}
// cur为空,说明没找到,parent是要插入的叶子结点
return make_pair(parent,-1);
}
4.InsertKey:把 key 插入 node 结点,同时带上 child 子节点
功能:向 node 结点插入关键字 key,并且把 child 子树挂在 key 的右侧 这个函数不做分裂 ,只管插入;插入之后外面判断结点满不满。
// node:待插入节点;key:关键字;child:这个key右侧的子节点指针
void InsertKey(Node* node, const K& key, Node* child)
{
// end:从当前已有最后一个关键字下标开始向前找
int end = node->_n - 1;
while (end >= 0)
{
// 如果当前关键字比key大 → 向后挪,腾位置
if (key < node->_keys[end])
{
node->_keys[end + 1] = node->_keys[end];
// 子指针同步后移
node->_subs[end + 2] = node->_subs[end+1];
--end;
}
else
{
break; // 找到位置,退出循环
}
}
// 把key放到空位 end+1
node->_keys[end + 1] = key;
// key的右侧子节点child放到 subs[end+2]
node->_subs[end + 2] = child;
node->_n++;
// 如果child不为空,设置它的父节点为当前node,更新 child 的父指针。
if (child)child->_parent = node;
}
5.Insert 核心插入函数
bool Insert(const K& key)
{
// 情况1:树是空,直接新建根放key
if (_root == nullptr)
{
_root = new Node;
_root->_keys[0] = key;
_root->_n++;
return true;
}
// 查找key是否已经存在,存在直接返回false
pair<Node*, int>ret = Find(key);
if (ret.second >= 0)return false;
// ret.first:找到的叶子节点,就是要插入的起点
Node* parent = ret.first;
K newKey = key;
Node* child = nullptr;
// 循环:向上处理分裂(你的版本是while循环,不是递归!重点)
while (1)
{
// 调用InsertKey:把newKey插入parent节点,child是newKey右边子树
InsertKey(parent, newKey,child);
// 插入之后,节点关键字数量 < M,没有满,直接结束
if (parent->_n < M)
{
return true;
}
else
{
// 插入之后节点满了!触发分裂
int mid = M / 2;
// 新建兄弟节点(分裂出来的右半部分)
Node* brother = new Node;
int j = 0;
int i = mid + 1;
// 把parent[mid+1 ... M-1]的关键字、子指针拷贝到brother
for (; i <= M - 1; i++)
{
brother->_keys[j] = parent->_keys[i];
brother->_subs[j] = parent->_subs[i];
// 拷贝过去的子节点,父指针改成brother
if (parent->_subs[i])
{
parent->_subs[i]->_parent = brother;
}
j++;
// 清空原节点已经拷贝走的数据
parent->_keys[i] = K();
parent->_subs[i] = nullptr;
}
// 单独处理最后一个子指针 subs[M]
brother->_subs[j] = parent->_subs[i];
if (parent->_subs[i])
{
parent->_subs[i]->_parent = brother;
}
parent->_subs[i] = nullptr;
brother->_n = j; // 新右节点关键字个数
// 原节点保留mid个关键字,减去brother的数量+中间midKey
parent->_n -= (brother->_n + 1);
// ========== 判断:分裂的是不是根 ==========
if (parent->_parent == nullptr)
{
// 分裂根节点:新建全局根,把中间关键字提上去
_root = new Node;
_root->_keys[0] = parent->_keys[mid];
parent->_keys[mid] = K(); // 原节点清空中间关键字
// 新根的两个孩子:分裂后的左节点parent、右节点brother
_root->_subs[0] = parent;
_root->_subs[1] = brother;
_root->_n = 1;
parent->_parent = _root;
brother->_parent = _root;
break;
}
else
{
// 不是根:中间关键字要向上插入到【parent的父节点】
newKey = parent->_keys[mid]; // 待向上提交的中间key
parent->_keys[mid] = K(); // 原节点清空中间key
child = brother; // 这个中间key的右孩子就是brother
parent = parent->_parent; // 向上走到父节点,下一轮循环继续InsertKey
}
}
}
return true;
}
- Find 找到叶子,准备插入
- 进入 while 循环:调用
InsertKey(parent, newKey, child) - 插入后如果节点没满,直接 return true
- 如果满了,分裂:
- 把 mid+1 后面所有 key + 子指针拷贝给 brother(新右节点)
- 原节点
parent砍掉后半部分,保留前 mid 个 key - 中间关键字
parent->_keys[mid]要往上提交 - 如果当前是根:新建根,树高度 + 1,退出循环
- 如果不是根:
newKey = 中间关键字child = brother(中间 key 的右子树就是新分裂出来的 brother)parent = parent->_parent回到 while 循环,再次调用 InsertKey 向上插入
实例:

例子 1:插入 50(不触发分裂)
Find(50),落到叶子49,53,ret.second=-1,不存在parent = 叶子(49,53),newKey=50,child=nullptr- 进入 while 循环
- 执行
InsertKey(parent,50,nullptr)- end 初始 = 1(keys=49,53)
- 50<53,53 后移,end=0
- key=50>49,break
- keysend+1=keys1=50,subs2=nullptr,
_n变成 3
- 判断
parent->_n <5→ 3<5,满足,return true 结束,叶子变成49,50,53
- 执行
例子 2:继续插入 52,叶子变成49,50,52,53,_n=4,还没满
继续插入 54:
-
Find 落到叶子
49,50,52,53,_n=4 -
while 循环:
InsertKey(parent,54,nullptr)- 插入完成,
_n=5,等于 M=5,节点满,进入分裂分支
- 插入完成,
-
mid = M/2 = 2
-
parent 现在 keys:
[49,50,52,53,54] -
i 从 mid+1=3 开始拷贝到 M-1=4
- i=3:key=53,subs 3 拷贝给 brother
- i=4:key=54,subs 4 拷贝给 brother
-
brother 的
_n=2,保存 53、54 -
parent->_n -= (2+1)→ 5-3=2,原节点保留前 2 个 key:49,50 -
midKey = parent->_keys2 =52
-
parent 不是根,所以:
newKey = 52; child = brother; parent = parent->_parent; // 走到父节点:23,35,47,55
-
-
回到 while 循环,再次执行
InsertKey(parent, newKey=52, child=brother)重点!这次 InsertKey 的 child 不再是 nullptr!child 是刚分裂出来的 brother 节点 含义:在父节点插入 52,52 右边的子树就是 brother -
插入 52 之后父节点
23,35,47,52,55,_n=5 == M,再次触发分裂,继续向上循环处理。
_InOrder 中序遍历
void _InOrder(Node* cur)
{
if (cur == nullptr)return;
int i = 0;
for (; i < cur->_n; i++)
{
_InOrder(cur->_subs[i]);//先遍历第i个子树
cout << cur->_keys[i] << " ";//输出当前关键字
}
_InOrder(cur->_subs[i]);//最后一个子树
}
void InOrder()
{
_InOrder(_root);
}
完整代码:
#include<iostream>
using namespace std;
template <class K,int M>
struct BTreeNode
{
K _keys[M];
BTreeNode<K, M>* _subs[M+1];
//多给一个空间,插入了再分裂
int _n;//实际存储多少个关键字
BTreeNode<K, M>* _parent;
BTreeNode()
:_parent(nullptr)
,_n(0)
{
for (int i = 0; i < M; i++)
{
_keys[i] = K();
_subs[i] = nullptr;
}
_subs[M] = nullptr;
}
};
template <class K, int M>
class BTree
{
typedef BTreeNode<K, M>Node;
public:
pair<Node*, int>Find(const K& key)
{
Node* parent = nullptr;//记录父亲,方便后面插入时能找到叶子节点
Node* cur = _root;
int i = 0;
while (cur)
{
//在单独一个节点找
while (i < cur->_n)
{
if (key < cur->_keys[i])
{
break;
}
else if (key > cur->_keys[i])
{
i++;
}
else
{
return make_pair(cur, i);
}
}
//去下一个分支
parent = cur;
cur = cur->_subs[i];
}
//找不到,把叶子节点parent带回去
return make_pair(parent,-1);
}
void InsertKey(Node* node, const K& key, Node* child)
{
int end = node->_n - 1;
while (end >= 0)
{
if (key < node->_keys[end])
{
node->_keys[end + 1] = node->_keys[end];
node->_subs[end + 2] = node->_subs[end+1];
--end;
}
else
{
break;
}
}
node->_keys[end + 1] = key;//比所有小到-1,正常情况
node->_subs[end + 2] = child;
node->_n++;
if (child)child->_parent = node;
}
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node;
_root->_keys[0] = key;
_root->_n++;
return true;
}
//key已经存在,就不插入
pair<Node*, int>ret = Find(key);
if (ret.second >= 0)return false;
Node* parent = ret.first;
K newKey = key;
Node* child = nullptr;
//检查满没有,满了就分裂,没有就结束
while (1)
{
InsertKey(parent, newKey,child);
if (parent->_n < M)
{
return true;
}
else
{
int mid = M / 2;
Node* brother = new Node;
int j = 0;
int i = mid + 1;
for (; i <= M - 1; i++)
{
brother->_keys[j] = parent->_keys[i];
brother->_subs[j] = parent->_subs[i];
if (parent->_subs[i])
{
parent->_subs[i]->_parent = brother;
}
j++;
//清楚拷贝走了的
parent->_keys[i] = K();
parent->_subs[i] = nullptr;
}
//最后剩一个右孩子,i==M
brother->_subs[j] = parent->_subs[i];
if (parent->_subs[i])
{
parent->_subs[i]->_parent = brother;
}
parent->_subs[i] = nullptr;
brother->_n = j;
parent->_n -= (brother->_n + 1);//3-2=1,还有一个中间节点
//说明刚刚分裂的是根节点
if (parent->_parent == nullptr)
{
//产生新的root
_root = new Node;
_root->_keys[0] = parent->_keys[mid];
parent->_keys[mid] = K();
_root->_subs[0] = parent;
_root->_subs[1] = brother;//新建的root,parent和brother都还没有维护
_root->_n = 1;
parent->_parent = _root;
brother->_parent = _root;
break;
}
else
{
//转换往parent->_parent插入中间节点和添加brother的分支,parent不需要再添加
newKey = parent->_keys[mid];
parent->_keys[mid] = K();
child = brother;
parent = parent->_parent;
}
}
}
return true;
}
void _InOrder(Node* cur)
{
if (cur == nullptr)return;
int i = 0;
for (; i < cur->_n; i++)
{
_InOrder(cur->_subs[i]);//左子树
cout << cur->_keys[i] << " ";//根
}
_InOrder(cur->_subs[i]);//最后右子树
}
void InOrder()
{
_InOrder(_root);
}
private:
Node* _root = nullptr;
};
void TestBTree()
{
int a[] = { 53,139,75,49,145,36,101 };
BTree<int, 3>t;
for (auto e : a)
{
t.Insert(e);
}
t.InOrder();
}
int main()
{
TestBTree();
return 0;
}
测试例子:
void TestBTree()
{
int a[] = { 53,139,75,49,145,36,101 };
BTree<int, 3>t;
for (auto e : a)
{
t.Insert(e);
}
t.InOrder();
}
运行结果:


