B树详解:从零构建平衡多路搜索树

🔥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 树强制叶子同层,所以查询稳定。

有序:结点内有序;关键字划分左右子树区间

两点: ① 同一个结点内部的关键字从小到大排好序 ② 任意关键字,左子树全部 <该关键字,右子树全部> 该关键字

看例子

  1. 结点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

查找逻辑:比如找 38,从根 17 → 大于 17 走右子树23,35,47,55;38 大于 35,小于 47,进入 35 和 47 之间的子树,找到38,45。

多路:m 阶 B 树,结点分支数有上下限

对于 m 阶 B 树的结点:

  1. 上限(所有结点通用,根、普通结点一样) 最多: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。

一步步实现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;
}
  1. Find 找到叶子,准备插入
  2. 进入 while 循环:调用InsertKey(parent, newKey, child)
  3. 插入后如果节点没满,直接 return true
  4. 如果满了,分裂:
    • 把 mid+1 后面所有 key + 子指针拷贝给 brother(新右节点)
    • 原节点parent砍掉后半部分,保留前 mid 个 key
    • 中间关键字parent->_keys[mid]要往上提交
    • 如果当前是根:新建根,树高度 + 1,退出循环
    • 如果不是根:
      • newKey = 中间关键字
      • child = brother(中间 key 的右子树就是新分裂出来的 brother)
      • parent = parent->_parent回到 while 循环,再次调用 InsertKey 向上插入

实例:

例子 1:插入 50(不触发分裂)

  1. Find(50),落到叶子49,53,ret.second=-1,不存在
  2. parent = 叶子(49,53),newKey=50,child=nullptr
  3. 进入 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:

  1. Find 落到叶子49,50,52,53,_n=4

  2. while 循环:InsertKey(parent,54,nullptr)

    • 插入完成,_n=5,等于 M=5,节点满,进入分裂分支
  3. 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
  4. 回到 while 循环,再次执行InsertKey(parent, newKey=52, child=brother) 重点!这次 InsertKey 的 child 不再是 nullptr!child 是刚分裂出来的 brother 节点 含义:在父节点插入 52,52 右边的子树就是 brother

  5. 插入 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();

}

运行结果:

相关推荐
FluxArt2 小时前
GPT Image 2.5 多图参考怎么用:Flux Art 生成与编辑教程
数据结构·gpt·算法
All for pursuit.2 小时前
【分治-2】215.数组中的第K个最大元素
数据结构·c++·算法·leetcode
CQU_JIAKE2 小时前
9.15【A】
算法·动态规划
成为深度学习高手2 小时前
DAG:沿时间与通道双相关建模的外生变量时序预测
网络·人工智能·深度学习·算法·机器学习·数据挖掘·时序数据库
阳明山水3 小时前
CEDAR双重解耦实现决策与干预分离
人工智能·深度学习·算法·机器学习·架构
w_zero_one3 小时前
链表(3)
java·数据结构·算法
Bruce_Liuxiaowei3 小时前
元组与具名元组:把「第 3 个字段」变成「price」
数据结构·python·语法·元组
Zootopia6263 小时前
美国载人飞船Crew-13明晚发射!
人工智能·算法·机器学习·数学建模·无人机·创业创新·信息与通信
zhangfeng11333 小时前
qjlDim 含义 TurboQuant QJL Quantized Johnson-Lindenstrauss,量化JL随机投影
人工智能·算法·ai编程·npu