首页 > 技术文章 > B/B+树及其相关原理笔记-持续更新中

savennist 2020-08-17 12:27 原文

1、B与B+树的初步了解及应用场景
维基百科对B树的定义为

在计算机科学中,B树(B-tree)是一种树状数据结构,
它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。
B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。
与自平衡二叉查找树不同,B-树为系统最优化大块数据的读和写操作。
B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。

这里引用一篇博客来初步帮助你了解两种数据结构以及它们在磁盘存储中的相关概念。当然,对于数据结构本身而言,下面将会收藏一些较好的博文供深入学习使用。

MySQL索引背后的数据结构及算法原理

B树与B+详解(图解+代码)


2、上一份代码吧

我也在这篇代码的博主博客评论区写了我的几个疑惑,希望得到回答。

#include <iostream>
#include <vector>
#include <queue>

using namespace std;


class BTree{
	static const int M = 2;
	struct BTNode{
		int keyNum;//keyNum指key的个数或者最后一个child指针的数组下标
		int key[2 * M - 1];  //关键字数组
		struct BTNode* child[2 * M];//孩子结点数组
		/*虽然在逻辑上是两个节点数组各个元素相间分布,但是由于受到语法限制,
		**我们只能够开辟两个数组以供使用
		**对于key[i]关键字,其左子树对应左指针节点child[i],其右子树对应右指针节点child[i+1]
		**这一点需要十分清晰 */
		bool isLeaf;
	};
	
	BTNode* root;
	//在插入时,保证pNode结点的关键字少于2*M-1个
	void InsertNonFull(BTNode* pNode, int key);
	//当child结点有2M-1个关键字时,分裂此结点
	void SplitChild(BTNode* parent, int i, BTNode* child);
	//两个关键字数量都小于M的结点合并
	void merge(BTNode* parent, BTNode* pNode1, BTNode* pNode2, int index);
	//找到比pNode结点第一个关键字小的最大的关键字,也就是前继结点
	int predecessor(BTNode* pNode);
	//找到后继结点
	int successor(BTNode* pNode);
	//pNode1向parent要一个结点key[index],parent向pNode0要一个结点,pNode1关键字个数为M-1
	void ExchangeLeftNode(BTNode *parent, BTNode* pNode0, BTNode* pNode1, int index);
	void ExchangeRightNode(BTNode* parent, BTNode* pNode1, BTNode *pNode2, int index);
	//删除,结点关键字个数不少于M
	void RemoveNonLess(BTNode* pNode, int key);
    //磁盘的写入与读取操作,不属于B树的知识范畴,不实现
	void DiskWrite(BTNode* pNode);
	void DiskRead(BTNode *pNode);
	BTNode* Search(BTNode* pNode, int key, int &index);
public:
	BTree();
	~BTree();
	BTNode* search(int key, int &index);
	void insert(int key);
	void remove(int key);
	//按层级打印。
	void PrintRow();
};
 
 
BTree::BTree()
{
	root = new BTNode();
	root->isLeaf = true;
	root->keyNum = 0;
	DiskWrite(root);
}
 
 
BTree::~BTree()
{
	struct BTNode* pNode;
	queue<struct BTNode*> q;
	q.push(root);
	while (!q.empty()){
		pNode = q.front();
		q.pop();
		//bfs的方式进行delete
		if (pNode->isLeaf)
			continue;//这里是不是应该在continue之前先delete?
		for (int i = 0; i <= pNode->keyNum; i++)
			q.push(pNode->child[i]);
		delete pNode;
	}
}
 
 
void BTree::DiskWrite(BTNode* pNode)
{
 
}
 
 
void BTree::DiskRead(BTNode *pNode)
{
 
}
 
 
BTree::BTNode* BTree::Search(BTNode* pNode, int key, int &index)
{
	int i = 0;
    //遍历pNode的每一个关键字,找到第一个大于等于key的下标
	while (i<pNode->keyNum&&key>pNode->key[i])i++;
	
	if (i < pNode->keyNum&&key == pNode->key[i]){//如果找到关键字,返回
		index = i;
		return pNode;
	}

    //在i==keyNum或(i<=keyNum&&key!=key[i])时,是扫描到叶子都没有找到,return NULL
    //在i<=keyNum&&key!=key[i]时,是key的值应当在child[i]子树里,继续递归搜索
	if (pNode->isLeaf)//已经搜到叶子结点,不存在
		return NULL;
	else{
		DiskRead(pNode->child[i]);
		return Search(pNode->child[i], key, index);//在第一个大于key值的孩子节点中递归搜索
	}
    //该函数默认返回的是key对应的关键字所在节点的指针
}
 
 
void BTree::InsertNonFull(BTNode* pNode, int key)
{//该函数可描述为:向pNode所指向的子树中递归插入关键字key
	int i = pNode->keyNum - 1;
	//注意,插入操作是发生在叶子节点的。
	//因为每一个非叶子节点的关键字一定都是从叶子节点中上升而来的
	if (pNode->isLeaf){//如果是叶子结点,直接插入
		while (i >= 0 && key < pNode->key[i]){
            //遵守关键字的递增排列,所以从最大关键字遍历,依次为新的关键字让位
			pNode->key[i + 1] = pNode->key[i];
			i--;
		}
		pNode->key[i + 1] = key;
		pNode->keyNum++;
		DiskWrite(pNode);
	}
	else {//如果是非叶子节点
		while (i >= 0 && key < pNode->key[i])
			i--;//找到第一个小于等于key的下标
		i++;//此时key关键字应当被插入以pNode->child[i]为根节点的子树中
		DiskRead(pNode->child[i]);
		if (pNode->child[i]->keyNum == 2 * M - 1){//判断孩子结点是否有2*M-1个关键字,有就需要分裂
			SplitChild(pNode, i, pNode->child[i]);//节点关键字个数超限,开始节点分裂
			if (key>pNode->key[i])//如果key比上移到父节点的元素大
				i++;
		}
		InsertNonFull(pNode->child[i], key);//已保证孩子结点关键字个数少于2*M-1个
		//之所以这么递归是因为每一个非叶子节点的关键字一定都是从叶子节点中上升而来的
	}
}

 
void BTree::SplitChild(BTNode* parent, int i, BTNode* child)
{//调用该函数时,child内的关键字个数达到了2*M-1个,需要分解成(M-1) + (M-1) + 1
	int j;
	struct BTNode* pNode = new BTNode();
	pNode->isLeaf = child->isLeaf;
	pNode->keyNum = M - 1;
	for (j = 0; j < M - 1; j++)//将child结点的 后M-1个 关键字赋给新节点
		pNode->key[j] = child->key[j + M];
	if (!child->isLeaf){//如果child不是叶子结点,将其后M个孩子结点赋给新节点。
		for (j = 0; j < M; j++)
			pNode->child[j] = child->child[j + M];
	}
	child->keyNum = M - 1;
 
	for (j = parent->keyNum; j > i; j--)
		parent->child[j + 1] = parent->child[j];//将child结点的父节点parent下标i以后的结点指针都向后移动一位,
	parent->child[j + 1] = pNode;//将新生成的结点当成parent的一个孩子
	for (j = parent->keyNum - 1; j >= i; j--)	//将i后面的关键字都向后移动一位
		parent->key[j + 1] = parent->key[j];
	parent->key[j + 1] = child->key[M - 1];//将孩子结点的中间结点移到父节点的指定位置
	parent->keyNum++;
	DiskWrite(parent);
	DiskWrite(pNode);
	DiskWrite(child);
}
 

void BTree::merge(BTNode* parent, BTNode* pNode1, BTNode* pNode2, int index)
{
	//合并关键字
	pNode1->key[M - 1] = parent->key[index];//这里直接使用M-1合适吗?????
	for (int i = 0; i < M - 1; i++)//将pNode2的关键字移到pNode1中
		pNode1->key[i + M] = pNode2->key[i];
	pNode1->keyNum = 2 * M - 1;
	//合并非叶子节点的child指针
	if (!pNode1->isLeaf){//如果不是叶子,将pNode2的孩子指针也移到pNode1中
		for (int i = 0; i < M; i++)
			pNode1->child[i + M] = pNode2->child[i];
	}
	//整理parent节点上的数据
	for (int i = index; i < parent->keyNum; i++){//将父节点index以后的关键字以及孩子指针都向前移动一位
		parent->key[i] = parent->key[i + 1];
		parent->child[i + 1] = parent->child[i + 2];
	}
	parent->keyNum--;
	delete pNode2;
}
 
//所谓前继节点,就是key[i]左侧指针child[i]所指向的B树中的最大关键字
int BTree::predecessor(BTNode* pNode)
{//在pNode节点指向的树中找到一个最大关键字
	while (!pNode->isLeaf)
		pNode = pNode->child[pNode->keyNum];
	return pNode->key[pNode->keyNum - 1];
}

//所谓后继节点,就是key[i]右侧指针child[i+1]所指向的B树中的最小关键字
int BTree::successor(BTNode* pNode)
{
	while (!pNode->isLeaf)
		pNode = pNode->child[0];
	return pNode->key[0];
}


void BTree::ExchangeLeftNode(BTNode *parent, BTNode* pNode0, BTNode* pNode1, int index)
{
	for (int i = pNode1->keyNum; i > 0; i--)
		pNode1->key[i] = pNode1->key[i - 1];//pNode1结点所有关键字向后移动一位
	pNode1->key[0] = parent->key[index];//第0个关键字来自父节点
	pNode1->keyNum++;
	parent->key[index] = pNode0->key[pNode0->keyNum - 1];//父节点的index处的关键字来自pNode0的最大关键字
 
	if (!pNode0->isLeaf){//如果不是叶子结点,
		for (int i = pNode1->keyNum; i > 0; i--)//将pNode1的孩子指针都向后移动一位,并将pNode0的最后一个孩子指针赋给它的第一个
			pNode1->child[i] = pNode1->child[i - 1];
		pNode1->child[0] = pNode0->child[pNode0->keyNum];
	}
 
	pNode0->keyNum--;
}
 
 
void BTree::ExchangeRightNode(BTNode* parent, BTNode* pNode1, BTNode *pNode2, int index)
{
	pNode1->key[pNode1->keyNum] = parent->key[index];
	pNode1->keyNum++;
	parent->key[index] = pNode2->key[0];
	for (int i = 0; i < pNode2->keyNum - 1; i++)
		pNode2->key[i] = pNode2->key[i + 1];
 
	if (!pNode2->isLeaf){
		pNode1->child[pNode1->keyNum] = pNode2->child[0];
		for (int i = 0; i < pNode2->keyNum; i++)
			pNode2->child[i] = pNode2->child[i + 1];
	}
	pNode2->keyNum--;
}


/*删除算法的思路-伪代码
RemoveNonLess(BTNode* pNode, int key):从 以pNode指向的节点为根的子树 中删除关键字key
如果 [pNode 指向叶子节点]:
	如果 [key 在 pNode 中]:
		直接删除key即可
	否则:
		没有找到 key
否则:
	如果 [key 在 pNode 中]:
		如果 [key左侧子节点中关键字个数>=M]:
			获取key的前继关键字pre
			用pre 替代 pNode中的key(赋值操作,覆盖即删除)
			递归删除左子树中的pre: RemoveNonLess(key左侧子节点指针,pre)
		否则如果 [key右侧子节点中关键字个数>=M]:
			获取key的后继关键字suc
			用suc 替代 pNode中的key(赋值操作,覆盖即删除)
			递归删除右子树中的suc: RemoveNonLess(key右侧子节点指针,suc)
		否则:
			合并 左子节点关键字,key,右子节点关键字 到 左子节点 中
			递归删除左子节点中的key: RemoveNonLess(key右侧子节点指针,key)
	否则:
		pNode1 = pNode->child[i];//获取key的左邻指针
		如果 [存在pNode1的左邻指针]: 将其设为pNode0
		如果 [存在pNode1的右邻指针]: 将其设为pNode2
		//如果都存在则逻辑结构为: pNode0 -- key[i-1] -- pNode1 -- key[i] -- pNode2
		//三个不同的指针代表pNode的三个相邻的不同子结点,且 key[i-1] <= key <= key[i]
		//下面通过判断pNode012的关键字个数,使pNode02支援pNode1
		如果 [pNode1的关键字个数 == M-1]:
			如果 [pNode0存在 && pNode0的关键字个数>=M]:
				将key[i-1]借入pNode1节点内
				将key[i-1]向pNode0借一个最大关键字
				根据pNode0是否为叶子节点调整结构
			否则如果 [pNode2存在 && pNode2的关键字个数>=M]:
				将key[i]借入pNode2节点内
				将key[i]向pNode1借一个最小关键字
				根据pNode2是否为叶子节点调整结构
			否则如果 [pNode0存在]:
				合并pNode0到pNode1
			否则:
				合并pNode2到pNode1
		进入pNode1子树中删除key: RemoveNonLess(pNode1,key)
*/
void BTree::RemoveNonLess(BTNode* pNode, int key)
{
	if (pNode->isLeaf){//到了叶子结点,直接删除
		int i = 0;
		while (i<pNode->keyNum&&key>pNode->key[i])
			i++;
		if (i < pNode->keyNum&&key == pNode->key[i]){
			while (i < pNode->keyNum - 1){
				pNode->key[i] = pNode->key[i + 1];
				i++;
			}
			pNode->keyNum--;
		}
		else {
			cout << "not found!" << endl;
		}
	}
	else {
		int i = 0;
		while (i < pNode->keyNum&&key > pNode->key[i])//找到第一个大于等于key的关键字
			i++;
		if (i < pNode->keyNum&&key == pNode->key[i]){//在结点中找到要删除的关键字
			struct BTNode* pNode1 = pNode->child[i];//key[i]左侧指针
			struct BTNode* pNode2 = pNode->child[i + 1];//key[i]右侧指针
			if (pNode1->keyNum >= M){//如果关键字左边的孩子结点的关键字数大于等于M
				int target = predecessor(pNode1);//将其前继结点移到pNode中
				pNode->key[i] = target;
				RemoveNonLess(pNode1, target);//递归删除target
			}
			else if (pNode2->keyNum >= M){//右边,同理
				int target = successor(pNode2);
				pNode->key[i] = target;
				RemoveNonLess(pNode2, target);
			}
			else {
				merge(pNode, pNode1, pNode2, i);//都小于M,合并(删除pNode2保留pNode1)
				RemoveNonLess(pNode1, key);
			}
		}
		else {//不在此结点中,则目标可能在pNode1中
			struct BTNode *pNode1 = pNode->child[i];
			struct BTNode *pNode0 = NULL;
			struct BTNode *pNode2 = NULL;
			if (i>0)
				pNode0 = pNode->child[i - 1];//pNode1的左邻指针
			if (i < pNode->keyNum)
				pNode2 = pNode->child[i + 1];//pNode1的右邻指针
			if (pNode1->keyNum == M - 1){//如果要删除的孩子结点关键字个数为M-1
				if (i > 0 && pNode0->keyNum >= M){//如果左邻指针的结点至少有M个关键字,向其借一个
					//pNode1向parent要一个结点key[index],parent向pNode0要一个结点,pNode1关键字个数为M-1
					ExchangeLeftNode(pNode, pNode0, pNode1, i - 1);
				}
				else if (i < pNode->keyNum&&pNode2->keyNum >= M){//同理,
					ExchangeRightNode(pNode, pNode1, pNode2, i);
				}
				else if (i>0){//相邻结点都只有M-1个关键字且左邻结点存在,则合并pNode1及其左邻子结点
					merge(pNode, pNode0, pNode1, i - 1);
					pNode1 = pNode0;
				}
				else{//相邻结点都只有M-1个关键字 且左邻结点不存在 且右邻结点存在,则合并pNode1及其右邻结点
					merge(pNode, pNode1, pNode2, i);
				}
				RemoveNonLess(pNode1, key);
			}
			else{
				RemoveNonLess(pNode1, key);
			}
		}
	}
}


BTree::BTNode* BTree::search(int key, int &index)
{
	return Search(root, key, index);
}
 
 
void BTree::insert(int key)
{
	struct BTNode* r = root;
	if (root->keyNum == 2 * M - 1){//根节点特殊处理,如果根节点关键字个数为2*M-1,
		struct BTNode* pNode = new BTNode();//新建一个结点作为新的根节点,并将现在的根节点作为
		root = pNode;//新根节点的孩子结点
		pNode->isLeaf = false;
		pNode->keyNum = 0;
		pNode->child[0] = r;
		SplitChild(pNode, 0, r);//孩子结点r有2*M-1个关键字
		InsertNonFull(pNode, key);
	}
	else
		InsertNonFull(r, key);
}
 
 
void BTree::remove(int key)
{
	if (root->keyNum == 1){//如果根节点只有两个孩子
		struct BTNode* pNode1 = root->child[0];
		struct BTNode* pNode2 = root->child[1];
		if (pNode1->keyNum == M - 1 && pNode2->keyNum == M - 1){//且两个孩子都只有M-1个关键字,合并
			//为什么感觉这里没有删除key的操作呢?????
			merge(root, pNode1, pNode2, 0);
			delete root;
			root = pNode1;
		}
		else {
			RemoveNonLess(root, key);
		}
	}
	else {
		RemoveNonLess(root, key);
	}
}
 
 
void BTree::PrintRow()
{
	struct BTNode* pNode;
	queue<struct BTNode*> q;
	q.push(root);
	while (!q.empty()){
		pNode = q.front();
		q.pop();
		cout << "[ ";
		for (int i = 0; i < pNode->keyNum; i++)
			cout << pNode->key[i] << " ";
		cout << "]" << endl;
		if (pNode->isLeaf)
			continue;
		for (int i = 0; i <= pNode->keyNum; i++)
			q.push(pNode->child[i]);
	}
}
 
int main(void)
{
	BTree tree;
	tree.insert('c');
	tree.insert('n');
	tree.insert('g');
	tree.insert('a');
	tree.insert('h');
	tree.insert('e');
	tree.insert('k');
	tree.insert('q');
	tree.insert('m');
	tree.insert('f');
 
	tree.insert('w');
	tree.insert('l');
	tree.insert('t');
	tree.insert('z');
	tree.insert('d');
	tree.insert('p');
	tree.insert('r');
	tree.insert('x');
	tree.insert('y');
	tree.insert('s');
	tree.remove('n');
	tree.remove('b');
	tree.PrintRow();
}

更新未完....

推荐阅读