[数据结构] 深入理解什么是B树及其模拟实现

定义

  • B树,又称M路平衡搜索树,一个结点上可以有多个key和映射规则
    B树

性质

  1. 根节点至少有两个孩子,所有的叶子节点都在同一层
  2. 每个分支节点都包含k-1个关键字和k个孩子(孩子比关键字多一个
  3. 每个叶子节点只包含k-1个关键字,其中ceil(m/2) ≤ k ≤ m
  4. 每个节点中的关键字从小到大排列
  5. 每个结点的结构为(n,A0,K1,A1,K2,A2,… ,Kn,An)
    其中,Ki(1≤i≤n)为关键字,且Ki<Ki+1;Ai(0≤i≤n) 为指向子树根结点的指针,且Ai所指子树所有结点中的关键字均小于Ki+1; n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。

B树的模拟实现

定义B树的结点

  • 设置数据的结构为<class K,int M>类型,K为磁盘地址(关键字),M为孩子的个数;
  • 若结点中关键字的个数定义为M,则其孩子的个数为M+1,但是为了方便插入以后再分裂,此处多给一个空间;
template <class K, int M>
struct BTreeNode{
	//关键字的个数比孩子小1
	K _keys[M];  //关键字
	BTreeNode<K, M>* _subs[M + 1];  //孩子结点
	BTreeNode<K, M>* _parent;  //指向其父结点的指针
	size_t _n;  //关键字的有效个数

	//初始化默认构造函数
	BTreeNode()
		:_parent(nullptr),
		_n(0)
	{
		for (size_t i = 0; i < M; i++){
			_keys[i] = K();
			_subs[i] = nullptr;
		}
	}
};

B树的基本框架

  • 初始时,根节点置空
class BTree{
public:
	typedef BTreeNode<K, M> Node;
private:
	Node* _root = nullptr;  
};

B树的基本操作

find查找操作

  • 小于该关键字或者与最后一个关键字都比较完了向下一层找,大于继续向后找,等于返回key所在结点及其下标;
pair<Node*, int> find(const K& key){
		Node* cur = _root;
		Node* parent = nullptr; //为了方便后续的插入操作,记录当前结点的父结点

		while (cur != nullptr){
			size_t i = 0; 
			while (i < cur->_n){
				if (key < cur->_keys[i]){
					break; //去左孩子里继续查找
				}
				else if (key > cur->_keys[i]){
					i++; //继续向后查找
				}
				else
					return pair<Node*, int>(cur, i);
			}

			parent = cur;  //更新父结点
			cur = cur->_subs[i];  //去左孩子里找
		}
		return pair<Node*, int>(parent, -1);  //没找到,下标返回-1
	}

insertkey插入操作

  • 在结点中插入关键字和对应右孩子:从后向前找待插入的位置,若key小,则当前位置的关键字向后挪动,关键字挪动的同时,其对应的右孩子也要挪动;否则插入key和其对应的右孩子child;
void insertKey(Node* node, const K& key, Node* child){
		//从右向左找key的待插入位置
		size_t i = node->_n - 1;
		for ( ; i >= 0; i--){
			if (key < node->_keys[i]){
				node->_keys[i + 1] = node->_keys[i];
				//挪动关键字的同时,其对应的右孩子也要挪动
				node->_subs[i + 2] = node->_subs[i + 1];
			}
			else{
				break;  //找到了待插入位置
			}
		}

		//插入key及其对应的右孩子
		node->_keys[i + 1] = key;
		node->_subs[i + 2] = child;
		if (child != nullptr){
			child->_parent = node;
		}
		node->_n++;
	}

insert插入操作

  • 空树-----直接插入根节点
  • key值已在树中-----不用插入
  • key值不在树中-----找待插入结点的位置-----insertKey-----判满-----若为满则分裂
  • 分裂-----分裂出自己的一半关键字和孩子给兄弟结点,中位数给父亲
  • 连接-----连接自己和父亲,连接兄弟和父亲
  • 注意:插入过程中可能会涉及到满的情况,需要向上生长,所以要记录每个结点的父结点
bool insert(const K& key){
		if (_root == nullptr){
			_root = new Node;
			_root->_keys[0] = key;
			_root->_n = 1;
			return true;
		}
		else{
			//1.先判断key是否已经存在
			pair<Node*, int> ret = find(key);
			if (ret.second != -1){
				return false;  //key已经存在
			}
			else{
				//key不存在,parent为要插入的叶子结点
				Node* parent = ret.first;
				K newkey = key;
				Node* child = nullptr;
				while (1){
					insertKey(parent, newkey, child);

					//2.判断是否满了
					if (parent->_n < M){ //不满,插入成功
						return true;  
					}
					//满了,进行分裂
					size_t mid = M / 2;
					//3.分裂一半[mid+1, M-1]数据给兄弟
					Node* brother = new Node;
					size_t j = 0; 
					size_t i = mid + 1;
					for ( ; i < M; i++){
						//拷贝一半的关键字+对应的左孩子给兄弟
						brother->_keys[j] = parent->_keys[i];
						brother->_subs[j] = parent->_subs[i];
						//清理自己对应的位置
						parent->_keys[i] = K();
						parent->_subs[i] = nullptr;
						j++;
					}
					//拷贝最后一个右孩子给兄弟
					brother->_subs[j] = parent->_subs[i];
					parent->_subs[i] = nullptr;
					//更新自己和兄弟的_n
					parent->_n -= (j+1);  //还有给父亲的中位数
					brother->_n += j;

					//4.连接:往自己的父亲结点中去插入中位数和兄弟孩子
					if (parent->_parent == nullptr){
						//父结点为空,刚刚分裂的是根结点
						_root = new Node;
						_root->_keys[0] = parent->_keys[mid];
						_root->_subs[0] = parent;
						_root->_subs[1] = brother;
						_root->_n++;
						//连接两个孩子结点
						parent->_parent = _root;
						brother->_parent = _root;
						return true;
					}
					else{
						//父结点非空,直接进行插入
						newkey = parent->_keys[mid];
						child = brother;
						parent = parent->_parent;
					}
				}
			}
		}
		return true;
	}

B树的验证

  • 中序遍历能够得到一个有序序列;
	void _inorder(Node* root){
		if (root == nullptr)
			return;
		for (size_t i = 0; i < root->_n; i++){
			_inorder(root->_subs[i]);  //左边孩子
			cout << root->_keys[i] << " ";
		}
		_inorder(root->_subs[root->_n]);  //最右边孩子
	}

B树的性能分析

  • 对于度为M的B树,每一个节点的子节点个数为M/2 ~(M-1)之间,因此树的高度应该在要logM-1N和logM/2N之间,在定位到该节点后,再采用二分查找的方式可以很快的定位到该元素,大大减少了读取磁盘的次数。
  • 只在内存中查找:单纯论树的高度,搜索效率而言,B树确实不错;
    但其有一些隐形的坏处:空间利用率低,消耗高;
    插入删除数据时,若分裂或合并结点,必然要挪动数据;虽然高度更低,但是在内存中而言,与哈希和平衡搜索树是一个量级的。
  • 所以,实质上,B树在内存中体现不出优势
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值