S31 B树详解

B树的由来

大量的数据首先存储位置在磁盘上,数据需要读取到内存中,再让内存和cpu进行交换,我们只需关心访问延迟,内存它的访问延迟纳秒为单位,磁盘的访问延迟在亳秒为单位,1秒==10~3毫秒==10~9纳秒,相当于磁盘的访问延迟是内存的100W倍。

怎么能降低磁盘访问次数?现在一个有效节点里面只存储一个有效值,如果一个有效节点可以存储多个有效值,这这时同样的数据量,则节点更加胖,但是组织架构会更加的扁平,也就是树的高度会降低。

B树的概念

1.平衡:所有的叶子节点都在同一层

2.有序:节点内部是有序递增的,且任意元素的左子树都小于它,右子树都大于它

3.多路:一个节点可以向外伸出M个手(M是阶数)

节点分支的上限:根节点上限是M/非根节点上限是M

节点分支的下限:根节点下限是2/非根节点上限是[M/2]

节点内部元素个数的上限:根节点上限是M-1个/非根节点上限是M-1个

节点内部元素个数的下限:根节点上限是1个/非根节点上限是[M/2]-1个

B树的节点定义

复制代码
typedef int ELEMTYPE;
#define M 5
typedef struct BTNode {
	int key_num;
	ELEMTYPE key_arr[M + 1]; 
	BTNode* prt_arr[M + 1];
}BTNode;

typedef struct BTree {
	struct BTNode* root;
}Btree;


typedef struct Result {
	bool tag;
	struct BTNode* pNode;
	int index;
}Result;

变量M为该B树为M阶B树

BTNode结构体中的key_num为该节点存储的多少个元素

key_arr为存储的值

prt_arr为其数组的左右子树。

Result结构体为返回的结构体节点数据。

B树的工具函数

购买节点函数

复制代码
BTNode* BT_Buy_Node()
{
	BTNode* p = (BTNode*)malloc(sizeof(BTNode));
	if (p == nullptr)return NULL;
	memset(p,0, sizeof(p));
	return p;
}

Result* BR_Buy_Node()
{
	Result* result = (Result*)malloc(sizeof(Result));
	if (result == NULL)
		exit(EXIT_FAILURE);

	result->index = 0;
	result->pNode = NULL;
	result->tag = false;

	return result;
}

B树的插入

插入主函数

复制代码
bool Insert_BTree(Btree* pTree, ELEMTYPE key)
{
	assert(pTree != NULL);
	if (pTree->root == NULL)
	{
		pTree->root = BT_Buy_Node();
		pTree->root->key_arr[0] = key;
		pTree->root->key_num = 1;
		return true;
	}
	Result* pr = Search_BTree(pTree, key);
	if (pr->tag)return true;
	memmove(pr->pNode->key_arr + pr->index + 1, pr->pNode->key_arr + pr->index, sizeof((pr->pNode->key_num - pr->index) * sizeof(ELEMTYPE)));
	pr->pNode->key_arr[pr->index] = key;
	pr->pNode->key_num++;
	if (pr->pNode->key_num == M) {
		BT_Insert_Adjust(pTree, pr->pNode);
	}
	return true;
}

插入调整函数

复制代码
void BT_Insert_Adjust(Btree* pTree, BTNode* Node)
{
	assert(pTree != NULL);
	while (Node->key_num >= M) {
		Node= BT_SplitNode(pTree, Node);
	}
	if (Node->key_num == 1) {
		pTree->root = Node;
	}
}

插入分裂函数

复制代码
BTNode* BT_SplitNode(BTree* pTree, BTNode* Node)
{
	BTNode* movenode = BT_Buy_Node();
	int r = 0;
	for (int i = Node->key_num / 2+1; i < Node->key_num; i++) {
		movenode->key_arr[r++] = Node->key_arr[i];
		movenode->key_num++;
	}
	Node->key_num -= movenode->key_num+1;
	r = 0;
	for (int i = M / 2 + 1; i <= Node->key_num; i++) {
		movenode->prt_arr[r++] = Node->prt_arr[i];
		//分裂的不是叶子节点
		if (movenode->prt_arr[i] != NULL) {
			movenode->prt_arr[i]->parent = movenode;
		}
	}
	BTNode* p = Node->parent;
	ELEMTYPE tmp = Node->key_arr[M / 2];
	if (p == NULL) {
		BTNode* newroot = BT_Buy_Node();
		newroot->key_arr[0] = tmp;
		newroot->key_num = 1;
		newroot->prt_arr[0] = Node;
		newroot->prt_arr[1] = movenode;
		Node->parent = newroot;
		movenode->parent = newroot;
		return newroot;
	}
	//注意r要从key_num开始,从0开始如果小于tmp直接跳出,并没由移动之前的值
	r = p->key_num;
	for (r; r > 0; r--) {
		if (p->key_arr[r-1] > tmp) {
			p->key_arr[r] = p->key_arr[r-1];
		}
		else {
			break;
		}
	}
	p->key_arr[r] = tmp;
	/*p->prt_arr[r + 2] = p->prt_arr[r + 1];*/
	memmove(p->prt_arr + 2 + r, p->prt_arr + r + 1, sizeof(p->prt_arr) * p->key_num - r);
	p->prt_arr[r+1] = movenode;
	p->key_num++;	
	movenode->parent = p;
	return p;
}

B树的删除

删除主函数

复制代码
bool Delete_BTree(Btree* pTree, ELEMTYPE key)
{
	assert(pTree != NULL);
	Result* pr = Search_BTree(pTree, key);
	if (!pr->tag) {
		return true;
	}
	//判断是否为叶子节点
	if (pr->pNode->prt_arr[0] != NULL) {
		BTNode* cat = pr->pNode->prt_arr[pr->index+1];
		while (cat->prt_arr[0] != NULL) {
			cat = cat->prt_arr[0];
		}
		pr->pNode->key_arr[pr->index] = cat->key_arr[0];
		pr->pNode = cat;
		pr->index = 0;
	}
	//此时删除的是叶子节点
	BTNode* p = pr->pNode;
	for (int i = pr->index+1; i < p->key_num; i++) {
		p->key_arr[i - 1] = p->key_arr[i];
	}
	p->key_num--;
	//此时有四种情况
	if ((p->parent == NULL && p->key_num >= 1) || (p->parent != NULL && p->key_num >= M / 2)) {
		return true;
	}
	if (p->parent == NULL && p->key_num == 0)
	{
		free(p);
		free(pr);
		pTree->root = NULL;
		return true;
	}
	BT_Delete_Adjust(pTree, p);
}

删除调整函数

复制代码
void Delete_Adjust(BTree* pTree, BTNode* Node)
{
    //0.assert
    //1.申请一个指针,用来指向Node的父节点
    BTNode* father = Node->parent;
    //2.方法1:通过Node地址的比较,判断出,当前Node在父节点中第几个下标上
    //  方法2:通过Node的元素比较,判断出,当前Node在父节点中第几个下标上
    //用2:思路就是父节点里面找第一个大于我当前Node节点里的第一个元素的值
    int i = 1;
    for (; i <= father->key_num; i++)
    {
        if (father->keys[i] > Node->keys[1])//找到了第一个大于我当前Node节点里的元素的值
        {
            break;
        }
    }

    //3.再申请两个指针,用来指向Node的左右兄弟
    //注意:左右兄弟是有可能不存在的
    BTNode* LeftBro = i - 2 >= 0 ? father->ptr[i - 2] : NULL;
    BTNode* RightBro = i <= father->key_num ? father->ptr[i] : NULL;

    //如果真的下溢出了,观察其左右兄弟是否存在且够借,如果够借,则问兄弟借,直接结束(口诀:父下来,兄上去)
    //4.尽量先看左兄弟是否存在且够借,如果存在且够借,则直接先问左兄弟借
    if (LeftBro != NULL && LeftBro->key_num > M / 2)
    {
        BorrowFrom_LeftBro(father, i - 1);
        return;
    }
    //5.再问右兄弟借(如果右兄弟存在且够借的情况下)
    else if (RightBro != NULL && RightBro->key_num > M / 2)
    {
        BorrowFrom_RightBro(father, i);
        return;
    }
    //6.执行到这里,就说明左右兄弟要么不存在,要么存在但是不够给我借元素
    else
    {
        //7.只能和左右兄弟进行合并了(口诀:父下左,右靠左)
        //7.5 准备工作:申请一个指针ptr,用来接收合并操作返回的合并的节点
        BTNode* ptr = NULL;
        //8.1 只有左兄弟存在-->只能和左兄弟合并
        //8.2 左右兄弟都存在-->和左兄弟合并
        if (LeftBro != NULL)
        {
            ptr = Merge_LeftBro(father, i - 1);
        }
        //8.3 只有右兄弟存在-- > 只能和右兄弟合并
        else
        {
            ptr = Merge_RightBro(father, i);
        }
        //注意:非根节点,不会存在左右兄弟都不存在的情况,至少得有一个兄弟存在
    
        //9.合并操作是会导致父节点缺少一个元素,所以可能会导致父节点出现连续的
        // 下溢出,则小心判断父节点,看是否需要继续处理
        //9.1 father是根节点 -> 溢出 -> 调整处理
        //10.特殊情况:如果合并操作导致根节点空了,没有元素了,则需要释放此时的根节点,让刚刚合并的节点看做新的根节点即可
        if (father->parent == NULL && father->key_num < 1)
        {
            free(father);
            pTree->root = ptr;
            ptr->parent = NULL;
        }
        //9.2 father是非根节点 -> 溢出 -> 调整处理
        else if (father->parent != NULL && father->key_num < M / 2)
        {
            Delete_Adjust(pTree, father);
        }
        //9.3 father是根节点 -> 没有溢出 -> 不需要调整处理
        //9.4 father是非根节点 -> 没有溢出 -> 不需要调整处理
        else
        {
            return;
        }
        
    }


}

左兄弟借函数

复制代码
void BT_BorrowFrom_LeftBro(BTNode* pp, int index)
{
	assert(index >= 0 && index < pp->key_num);
	BTNode* child = pp->prt_arr[index];
	BTNode* leftBro = pp->prt_arr[index - 1];
	for (int i = child->key_num-1; i >= 0; i--)
		child->key_arr[i + 1] = child->key_arr[i];
	child->key_arr[0] = pp->key_arr[index];
	if (leftBro->prt_arr[0] != NULL)
	{
		for (int i = child->key_num; i >= 0; i--)
			child->prt_arr[i + 1] = child->prt_arr[i];

		child->prt_arr[0] = leftBro->prt_arr[leftBro->key_num];
	}
	pp->key_arr[index] = leftBro->key_arr[leftBro->key_num-1];
	child->key_num += 1;
	leftBro->key_num -= 1;
}

右兄弟借函数

复制代码
//8.从右兄弟借
void BorrowFrom_RightBro(BTNode* pp, int index)
{
    assert(index >= 1 && index <= pp->key_num);

    //1.先通过pp和index找到自己和右兄弟节点
    BTNode* child = pp->ptr[index-1];
    BTNode* rightBro = pp->ptr[index];

    //2.将父节点的值拉下来(口诀:父下来)
    child->keys[child->key_num+1] = pp->keys[index];

    //3.将兄弟节点的值,挪上去到父节点
    pp->keys[index] = rightBro->keys[1];

    //4.若如果兄弟不是叶子结点(换句话说,兄弟有子树)
    //则把右兄弟的第一个孩子指针域,挪动到自己的最后一个指针域位置
    if (rightBro->ptr[0] != NULL)
    {
        child->ptr[child->key_num+1] = rightBro->ptr[0];
    }

    //5.修正右兄弟节点的元素值和孩子指针域
    for (int i = 2; i <= rightBro->key_num; i++)
        rightBro->keys[i - 1] = rightBro->keys[i];
    for (int i = 1; i <= rightBro->key_num; i++)
        rightBro->ptr[i - 1] = rightBro->ptr[i];


    //6.修正更新各个节点的有效元素个数
    child->key_num += 1;
    rightBro->key_num -= 1;
}

左兄弟合并函数

复制代码
BTNode* Merge_LeftBro(BTNode* pp, int index)
{
    //0.assert
    assert(pp != NULL);

    // 口诀:父下左,右靠左
    //1.申请两个指针Node和LeftNode用来指向自己和需要和自己合并的左兄弟
    BTNode* LeftNode = pp->ptr[index-1];
    BTNode* Node = pp->ptr[index];

    //2.将Node和LeftNode在父节点中夹着的那个元素,挪下来,挪到左侧的LeftNode节点的屁股后边(父下左)
    LeftNode->keys[LeftNode->key_num+1] = pp->keys[index];

    //3.更新一下LeftNode节点的元素个数,因为一会后边要用到LeftNode.key_num这个参数
    LeftNode->key_num++;

    //4.再将右侧的Node的全部有效元素给到左侧的LeftNode
    for (int i = 1; i <= Node->key_num; i++)//i指向的合并的右侧节点Node的元素下标
    {
        LeftNode->keys[LeftNode->key_num + i] = Node->keys[i];
    }
    
    //5.再将右侧的Node的全部孩子指针域给到左侧的LeftNode自己(特别注意:如果右侧节点的
    // 孩子指针不为NULL,也就是说其孩子指针指向的是确确实实存在的节点,那么你需要考虑
    // 把它这下孩子的双亲指针也修正一下)
    for (int i = 0; i <= Node->key_num; i++)//i指向的合并的右侧节点Node的孩子指针域下标
    {
        if (Node->ptr[i] != NULL)
        {
            LeftNode->ptr[LeftNode->key_num+i] = Node->ptr[i];
            Node->ptr[i]->parent = LeftNode;
        }
    }
    //6.修正父节点现有的元素及其的剩余的孩子指针域统一向前挪动一下
    for (int i = index + 1; i <= pp->key_num; i++)
    {
        pp->keys[i - 1] = pp->keys[i];
        pp->ptr[i - 1] = pp->ptr[i];
    }


    //7.修正各个其他节点的有效元素个数
    LeftNode->key_num += Node->key_num;
    pp->key_num -= 1;

    //8.释放右侧的Node节点
    free(Node);

    //9.将合并之后的节点返回出去(返回的是左侧的LeftNode)
    return LeftNode;
}

右兄弟合并函数

复制代码
BTNode* Merge_RightBro(BTNode* pp, int index)
{
    //0.assert
    assert(pp != NULL);
    
    // 口诀:父下左,右靠左
    //1.申请两个指针Node和RightNode用来指向自己和需要和自己合并的右兄弟
    BTNode* Node = pp->ptr[index - 1];
    BTNode* RightNode = pp->ptr[index];

    //2.将Node和RightNode在父节点中夹着的那个元素,挪下来,挪到左侧的Node节点的屁股后边(父下左)
    Node->keys[Node->key_num + 1] = pp->keys[index];

    //3.更新一下Node节点的元素个数,因为一会后边要用到Node.key_num这个参数
    Node->key_num++;//这一步非常重要 一定在这里修正好

    //4.再将右侧的RightNode的全部有效元素给到左侧的Node自己
    for (int i = 1; i <= RightNode->key_num; i++)
    {
        Node->keys[Node->key_num+i] = RightNode->keys[i];
    }

    //5.再将右侧的RightNode的全部孩子指针域给到左侧的Node自己(特别注意:如果右侧节点的
    // 孩子指针不为NULL,也就是说其孩子指针指向的是确确实实存在的节点,那么你需要考虑
    // 把它这下孩子的双亲指针也修正一下)
    for (int i = 0; i <= RightNode->key_num; i++)
    {
        if (RightNode->ptr[i] != NULL)
        {
            RightNode->ptr[i]->parent = Node;
            Node->ptr[Node->key_num + i] = RightNode->ptr[i];
        }
    }

    //6.修正父节点现在的元素及其的剩余的孩子指针域统一向前挪动一下
    for (int i = index + 1; i <= pp->key_num; i++)
    {
        pp->keys[i-1] = pp->keys[i];//挪动剩余元素
        pp->ptr[i-1] = pp->ptr[i];//挪动剩余孩子指针域
    }
    
    //7.修正各个其他节点的有效元素个数
    Node->key_num = Node->key_num + RightNode->key_num;
    pp->key_num -= 1;

    //8.释放右侧的RightNode节点
    free(RightNode);

    //9.将合并之后的节点返回出去(左侧的Node)
    return Node;

}

B树的查找

复制代码
Result* Search_BTree(BTree* root, ELEMTYPE val)
{
	assert(root != NULL);
	BTNode* p = root->root;
	BTNode* pp = NULL;
	int i = 0;
	Result* node = BR_Buy_Node();
	while (p != nullptr) {
		 i = 0;
		while (i < p->key_num) {
			if (val > p->key_arr[i]) {
				i++;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
			}
			else if (val == p->key_arr[i]) {
				node->tag = true;
				node->index =  i;
				node->pNode = p;
				return node;
			}
			else {
				pp = p;
				p = p->prt_arr[i];
				break;
			}
		}
		if (i == p->key_num) {
			pp = p;
			p = p->prt_arr[i];
		}
	}
	node->pNode = pp;
	node->index = i;
	return node;
}

B树的打印

复制代码
void Show_InOrder1(BTNode* node)
{
	if (node == nullptr)return;
	int i = 0;
	for (; i < node->key_num; i++) {
		if (node->prt_arr[i] != nullptr) {
			Show_InOrder1(node->prt_arr[i]);
			continue;
		}
		printf("%d ", node->key_arr[i]);
	}
	if (node->prt_arr[i] != nullptr) {
		Show_InOrder1(node->prt_arr[i]);
	}
}
相关推荐
Boop_wu2 小时前
[Java 数据结构] 图(1)
数据结构·算法
无尽的罚坐人生2 小时前
hot 100 128. 最长连续序列
数据结构·算法·贪心算法
Savior`L2 小时前
基础算法:模拟、枚举
数据结构·c++·算法
white-persist3 小时前
【内网运维】Netsh 全体系 + Windows 系统专属命令行指令大全
运维·数据结构·windows·python·算法·安全·正则表达式
TechNomad3 小时前
哈希表的原理详解
数据结构·哈希算法
蒙奇D索大3 小时前
【数据结构】排序算法精讲 | 快速排序全解:高效实现、性能评估、实战剖析
数据结构·笔记·学习·考研·算法·排序算法·改行学it
@小码农3 小时前
2025年12月 GESP认证 图形化编程 一级真题试卷(附答案)
开发语言·数据结构·算法
小袁顶风作案4 小时前
leetcode力扣——27.移除元素、26.删除有序数组的重复项、80.删除有序数组中的重复项 II
数据结构·算法·leetcode
曾几何时`4 小时前
滑动窗口(十五)2962. 统计最大元素出现至少 K 次的子数组(越长越合法型)
数据结构·算法