数据结构B树的实现

B树

B 树是一种多路平衡搜索树 (Multiway Balanced Search Tree),其设计目标在于减少磁盘读写次数 ,从而提高大规模数据处理时的性能 。不同于二叉树,B 树的每个节点可以存储多个关键字和多个子节点指针,这使得它能在较少的层数中容纳更多数据。正因为这种结构,B 树常常用于外部存储系统,比如数据库索引和文件系统

1. B树的结构特点

  1. 根节点至少有两个孩子。
  2. 每个分支节点 包含 k-1 个关键字和 k 个孩子,且 k 满足 ceil ( m / 2 ) ≤ k ≤ m \text {ceil}(m/2) \le k \le m ceil(m/2)≤k≤m (m 为 B 树的阶数,ceil() 表示向上取整函数)。
  3. 每个叶子节点 包含 k-1 个关键字,且 k 满足 ceil ( m / 2 ) ≤ k ≤ m \text {ceil}(m/2) \le k \le m ceil(m/2)≤k≤m 。
  4. 所有的叶子节点都在同一层。
  5. 若一个节点有 k − 1 k-1 k−1 个关键字: a 1 , a 2 , ... , a k − 1 a_1,a_2, \dots ,a_{k-1} a1,a2,...,ak−1 。 k k k 个子节点: c 1 , c 2 , ... , c k c_1,c_2, \dots ,c_k c1,c2,...,ck 。则所有在子节点 c 1 c_1 c1 中的关键字都小于 a 1 a_1 a1 ,在子节点 c i ( 2 ≤ i ≤ k − 1 ) c_i \space (2 \le i \le k-1) ci (2≤i≤k−1) 中的所有关键字都介于 a i − 1 a_{i-1} ai−1 和 a i a_i ai 之间,而子节点 c k c_k ck 中的关键字均大于 a k − 1 a_{k-1} ak−1 。

2. B树的插入

B 树也是一种左孩子小于自身,右孩子大于自身的搜索树结构。B 树在插入时,是在叶子节点上插入新的值,但若某个叶子节点的关键字满了,就会发生分裂。

插入过程:

  1. 寻找合适的叶子节点

    从根节点开始,按左小右大的顺序比较,沿着对应的子节点指针一路向下,直到找到目标叶子节点。

  2. 在叶子节点中插入关键字

    在 B 树中,叶子节点会包含 k-1 个关键字,在叶子节点上也要按左小右大的规则排序,所以在叶子节点上关键字可能会发生挪动。

  3. 节点溢出处理(分裂)

    如果在插入关键字后,该叶子节点的关键字满了,就要进行节点分裂:

    • 找到该叶子节点的中间关键字,即找到中位数
    • 叶子节点保留中位数左侧的所有的所有关键字。
    • 中位数提升到父结点中,作为父结点的关键字。
    • 中位数右侧的所有关键字插入到新的节点中,并作为左侧关键字节点的兄弟节点。
  4. 向上递归分裂

    如果中位数提升到父结点后,父结点的关键字也满了,则父结点也进行相同的分裂操作。如果向上递归分裂一直递归到根节点 ,且根节点也要进行分裂,此时树的高度会增加一层,新创建的根节点只包含刚提升的中位数和两个子节点指针。

    在线模拟B树插入、查找、删除过程网站

2.1 示例

  1. 假设有一颗 4 阶段 B 树(每个分支节点有 4 个孩子和 3 个关键字),插入 20:

  2. 现在再分别插入 10 和 30,插入 10 时, 10 < 20,故 20 要向右挪动,30 插入在 20 的右侧,20 变成中位数:

  3. 现在由于根节点满了,需要进行节点溢出处理:20 作为新的根节点的中位数;原节点保留 10,并变为新根节点的左孩子;30 插入到新分支节点中,并作为原节点的兄弟节点:

    可以看到此时根节点发生分裂,树的高度增加 1,此后的插入操作,都是在 L1 层中插入,当左右孩子发生节点溢出处理时,才会有数据插入到根节点,根节点再次满后,树的高度又会增加 1。

  4. 现在再插入 7 和 15,由于 7 和 15 比 20 小,插入到左孩子中:

  5. 现在根节点的左孩子满了,需要进行节点溢出处理:10 作为中位数提升到父结点中;原节点保留 7;15 插入到新分支节点中,并作为原节点的兄弟节点:

2.2 代码实现

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;
template<class K, int M = 4>
class BTreeNode
{
public:
	K _keys[M - 1];	//关键字宿主
	BTreeNode<K, M>* _subs[M];//孩子节点数组
	BTreeNode<K, M>* _parent;	//指向父结点
	size_t _size;	//节点中有效元素的个数

	BTreeNode() :_parent(nullptr), _size(0)
	{
		for (size_t i = 0; i < M; i++)
		{
			_subs[i] = nullptr;
		}
	}
};

template<class K, int M>
class BTree
{
public:
	typedef BTreeNode<K, M> Node;

	BTree() :_root(nullptr) {}


	pair<Node*, int> Find(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;

		while (cur)
		{
			//在当前节点的关键字宿主中匹配
			size_t i = 0;
			while (i < cur->_size)
			{
				if (key < cur->_keys[i])
				{
					break;
				}
				else if (key > cur->_keys[i])
				{
					i++;
				}
				else
				{
					return make_pair(cur, i);
				}
			}
			parent = cur;
			//若key在该节点孩子节点中,它必是比_key[i-1]大,比_key[i]小
			cur = cur->_subs[i];
		}
		//若key不能存在,返回应插入key的位置的父结点
		return make_pair(parent, -1);
	}

	void InsertKey(Node* node, const K& key, Node* child)
	{
		int end = node->_size - 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;
		node->_subs[end + 2] = child;
		if (child)
		{
			child->_parent = node;
		}
		node->_size++;
	}

	pair<Node*, bool> Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node;
			_root->_keys[0] = key;
			_root->_size++;
			return make_pair(_root, true);
		}

		//Find查询要插入的key是否已经存在,如果不能存在顺便将要插入的位置带回
		pair<Node*, int> ret = Find(key);
		if (ret.second >= 0)
		{
			return make_pair(ret.first, false);
		}

		Node* parent = ret.first;
		Node* retNode = parent;	//作为返回值返回
		K newKey = key;
		Node* child = nullptr;
		while (1)
		{
			InsertKey(parent, newKey, child);
			if (parent->_size < M-1)
			{
				return make_pair(retNode, true);
			}
			else
			{
				//将中位数的右边分给兄弟节点
				size_t mid = (M - 1) / 2;
				Node* brother = new Node;
				size_t j = 0;
				size_t i = mid + 1;
				for (; i < M-1; i++)
				{
					brother->_keys[j] = parent->_keys[i];
					brother->_subs[j] = parent->_subs[i];
					//注意这里的parent和brother是兄弟关系
					if (parent->_subs[i])
					{
						parent->_subs[i]->_parent = brother;
					}
					j++;

					parent->_keys[i] = INT_MIN;
					parent->_subs[i] = nullptr;
				}
				//因为关键字比孩子少一个,所以走完循环还有一个孩子需要赋值
				brother->_subs[j] = parent->_subs[i];
				if (parent->_subs[i])
				{
					parent->_subs[i]->_parent = brother;
				}
				parent->_keys[i] = INT_MIN;
				parent->_subs[i] = nullptr;

				brother->_size = j;
				//parent->_size -= (brother->_size + 1);
				parent->_size = mid;

				//中位数提升到父结点中
				K midKey = parent->_keys[mid];
				parent->_keys[mid] = INT_MIN;

				//如果溢出处理的是根节点
				if (parent->_parent == nullptr)
				{
					_root = new Node;
					_root->_keys[0] = midKey;
					_root->_subs[0] = parent;
					_root->_subs[1] = brother;
					_root->_size = 1;

					parent->_parent = _root;
					brother->_parent = _root;
					break;
				}
				else
				{
					//向上调整
					newKey = midKey;
					child = brother;
					parent = parent->_parent;
				}
			}
		}
		return make_pair(retNode, true);
	}

	void _InOrder(Node* cur)
	{
		if (cur == nullptr)
		{
			return;
		}
		size_t i = 0;
		for (; i < cur->_size; i++)
		{
			_InOrder(cur->_subs[i]);
			cout << cur->_keys[i] << " ";
		}
		_InOrder(cur->_subs[i]);
	}

	void InOrder()
	{
		_InOrder(_root);
	}

private:
	Node* _root;
};

int main()
{
	int a[] = { 53, 139, 75, 49, 145, 36, 101 };
	BTree<int, 4> t;
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.InOrder();
	return 0;
}

3. B+树

B+ 树是 B 树的优化版本,B 树的结构相对比较复杂,比如一颗 4 阶的 B 树,它的结构为每个节点拥有 4 个孩子指针, 3 个关键字索引,在平衡状态下每个节点最多存有 2 个关键字,这样复杂的结构让其在代码处理上更麻烦。所以 B+ 树简化了它的结构,并专门在数据存储上做了新的优化。

3.1 B+树的结构

B+ 树规定:

  1. 非叶子节点只存储数据的索引,叶子节点存放关键字内容。
  2. 分支节点的索引和孩子指针数量相等,叶子节点存储更多的关键字并且含有一个指向兄弟节点的指针。
  3. 分支节点的索引 A i A_i Ai 是其对应孩子节点 C i C_i Ci 的最小关键字, C i C_i Ci 的关键字范围不会大于父结点的索引 A i + 1 A_{i+1} Ai+1 。
  4. 除了根节点外,每个节点至少有 m / 2 m/2 m/2 个孩子, m m m 为 B+ 树的阶数。
  5. 当分支节点溢出时,需要进行节点分裂处理,需要分为叶子节点和分支节点两种情况处理。

注意,B+ 树的叶子节点能存储多少个关键字并没有明确的规定。

3.2 B+树的节点分裂

3.2.1 叶子节点分裂

设定叶子节点中的关键字容量为 L L L 。

  1. 将叶子节点中前 L / 2 L/2 L/2 个关键字保留在原节点,其他关键字放入新叶子节点中。
  2. 新分裂的节点作为原节点的兄弟节点,更新叶子节点中指向兄弟节点的指针。
  3. 将新节点的最小关键字插入到父结点的索引中,若父结点因此溢出,则需要进行分支节点溢出处理。
3.2.2 分支节点分裂

分支节点的分裂与 B 树相同。

  1. 找到该叶子节点的中间关键字,即找到中位数
  2. 叶子节点保留中位数左侧的所有的所有关键字。
  3. 中位数提升到父结点中,作为父结点的关键字。
  4. 中位数右侧的所有关键字插入到新的节点中,并作为左侧关键字节点的兄弟节点。
  5. 向上递归分裂处理,若根节点需要进行分裂,则整棵树的高度增加 1。
相关推荐
尤物程序猿11 分钟前
【2025面试Java常问八股之redis】zset数据结构的实现,跳表和B+树的对比
数据结构·redis·面试
2301_8076114912 分钟前
77. 组合
c++·算法·leetcode·深度优先·回溯
安冬的码畜日常30 分钟前
【AI 加持下的 Python 编程实战 2_10】DIY 拓展:从扫雷小游戏开发再探问题分解与 AI 代码调试能力(中)
开发语言·前端·人工智能·ai·扫雷游戏·ai辅助编程·辅助编程
朝阳5811 小时前
Rust项目GPG签名配置指南
开发语言·后端·rust
微网兔子1 小时前
伺服器用什么语言开发呢?做什么用什么?
服务器·c++·后端·游戏
朝阳5811 小时前
Rust实现高性能目录扫描工具ll的技术解析
开发语言·后端·rust
程高兴1 小时前
基于Matlab的车牌识别系统
开发语言·matlab
YuforiaCode1 小时前
第十三届蓝桥杯 2022 C/C++组 修剪灌木
c语言·c++·蓝桥杯
YOULANSHENGMENG1 小时前
linux 下python 调用c++的动态库的方法
c++·python
牛马baby1 小时前
Java高频面试之并发编程-07
java·开发语言·面试