看数据结构写代码(58) B-树

B-树 一种 自平衡的 多路 查找树。它在文件系统里很有用。

一个m阶的B-树,要么是空树,要么是满足这些特性的树。、

1.树 最多 有 m个分支。

2.树的根 最少 两个子树。

3. 树的非终端叶子节点 最少  m/2 向上 取整   个 子树。

4.所有叶子节点 都在 一层。

它的节点 结构:  (N,P0,K1,P1,K2,p2......Kn,Pn)

其中 N 是 节点的 关键字个数,p0~pn 是 指向子树的指针,K1~Kn是关键字

#define M		3 //B树的阶数..

typedef int keyType;

typedef struct BTreeNode{
	int keyNum;//关键字的个数.
	BTreeNode * ptr[M+1];//指针数组,(总比 关键字 多一个).
	keyType key[M+1];//关键字数组,第0个没用,从 第1个开始,
	BTreeNode * parentNode;//节点的父亲..
}*BTree;

根据上面的特性,总结:

1.节点中 分支树,最大为 阶数,最小 为 m/2 向上 取整(根节点最小为2,只有一个根节点 的树 可以没有子树)

2.所有结构中,关键字个数  =  指向子树的指针树 -1,所以关键字 最大 为 M-1,最少 为  上面 所说的 最小值 -1.

3.K1~Kn,顺序排放。

4.假设 k 是 节点的 第 x个 关键字,那么 P(x-1)指向的 子树中的 的 关键字 都比 k小,P(x)指向的子树指向的关键字 都比 k大。

B-树的 查找,分为 两部分,一部分 是顺指针查找节点 ,一部分 是 子 节点里 查找。下面的 search函数 就是 在 子节点里查找 关键字。

struct Result{//查找的结果
	int tag;//0查找失败,1查找成功
	int index;//位置
	BTreeNode * p;//指向查找成功的指针或者插入位置的 指针
};
//查找到:返回k的位置; 没查找到:返回 k值的 搜索指针位置。
int search(BTree t,keyType k){
	if (t != NULL){
		for (int i = 1; i <= t->keyNum; i++){
			if (t->key[i] == k){//找到了返回当前索引
				return i;
			}
			else if(t->key[i] > k){//没找到,返回 前一个位置
				return i-1;
			}
		}
		return t->keyNum;//所有节点 都比 k 小
	}
	return 0;
}
//查找
Result searchBTree(BTree t,keyType k){
	BTree p = t,q = NULL;
	bool isFind = false;
	int index = 0;
	while (p != NULL){
		index = search(p,k);
		if (index >= 1 && p->key[index] == k ){
			isFind = true;
			break;
		}
		else{
			q = p;
			p = p->ptr[index];
		}
	}
	if (isFind){//查找到了
		Result r ={1,index,p};
		return r;
	}
	else{
		Result r ={0,index,q};
		return r;
	}
}

下面 主要 说 B-树的 插入 算法。

1.如果节点 存在,不插入

2.节点不存在,插入 节点

3.如果 插入后, 节点  的 关键字个数 大于 M-1个,则 需要 分裂 ,具体分裂如下:

如果 造成 父节点 关键字个数 超过 M-1,则 需要 分裂。

完整代码如下:

// BTree.cpp : 定义控制台应用程序的入口点。
//B-树:多路自平衡查找树

#include "stdafx.h"
#include <cstdlib>
#include <cmath>
#include "queue.h"
#define M		3 //B树的阶数..

typedef int keyType;

typedef struct BTreeNode{
	int keyNum;//关键字的个数.
	BTreeNode * ptr[M+1];//指针数组,(总比 关键字 多一个).
	keyType key[M+1];//关键字数组,第0个没用,从 第1个开始,
	BTreeNode * parentNode;//节点的父亲..
}*BTree;

//获取一个节点,这样 的 初始化是个 好习惯.
BTreeNode * makeNode(){
	BTreeNode * node = (BTreeNode*)malloc(sizeof(BTreeNode));
	node->parentNode = NULL;
	for (int i = 0; i <= M; i++){
		node->ptr[i] = NULL;
	}
	node->keyNum = 0;
	return node;
}

void initTree(BTree *tree){
	*tree = NULL;
}

struct Result{//查找的结果
	int tag;//0查找失败,1查找成功
	int index;//位置
	BTreeNode * p;//指向查找成功的指针或者插入位置的 指针
};
//查找到:返回k的位置; 没查找到:返回 k值的 搜索指针位置。
int search(BTree t,keyType k){
	if (t != NULL){
		for (int i = 1; i <= t->keyNum; i++){
			if (t->key[i] == k){//找到了返回当前索引
				return i;
			}
			else if(t->key[i] > k){//没找到,返回 前一个位置
				return i-1;
			}
		}
		return t->keyNum;//所有节点 都比 k 小
	}
	return 0;
}
//查找
Result searchBTree(BTree t,keyType k){
	BTree p = t,q = NULL;
	bool isFind = false;
	int index = 0;
	while (p != NULL){
		index = search(p,k);
		if (index >= 1 && p->key[index] == k ){
			isFind = true;
			break;
		}
		else{
			q = p;
			p = p->ptr[index];
		}
	}
	if (isFind){//查找到了
		Result r ={1,index,p};
		return r;
	}
	else{
		Result r ={0,index,q};
		return r;
	}
}
//将 p 插入 到  t->ptr[index+1]
//将 k 插入到 t->key[index+1];
void insert(BTree t,int index,keyType k,BTree p){
	//index 后面的移开
	for (int i = t->keyNum; i > index; i--){
		t->key[i+1] = t->key[i];
		t->ptr[i+1] = t->ptr[i];
	}
	t->key[index+1] = k;
	t->ptr[index +1] = p;
	t->keyNum ++;
}

//将 index 后部分分裂出 一个 新的节点
//指针 复制 :index 到 keyNum
//关键字复制: index +1 到 keyNum
void split(BTree t,int index,BTree * newNode){
	*newNode = makeNode();
	int count = 0;
	for (int i = index; i <= t->keyNum; i++){
		if (i > index){////关键字复制: index +1 到 keyNum
			count++;
			(*newNode)->key[count] = t->key[i];//从1 开始.
		}
		BTree child = (*newNode)->ptr[count] = t->ptr[i];
		//忘记写了,需要 把 带走的节点 设置 新的 父节点
		if (child != NULL){
			child->parentNode = *newNode;
		}
		t->ptr[i] = NULL;//设置原有指针为NULL
	}
	//下面两句漏写..
	(*newNode)->parentNode = t->parentNode;
	t->keyNum = index -1;//原节点 数量 减少.
	(*newNode)->keyNum = count;
}
//新的根节点(之前为 空树,或者 分裂出了 一个 头节点)
void newRoot(BTree * t,keyType k,BTree left,BTree right){
	BTree p = *t = makeNode();
	p->key[1] = k;
	p->ptr[0] = left;
	p->ptr[1] = right;
	p->keyNum = 1;
	if (left != NULL){
		left->parentNode = p;
	}
	if (right != NULL)
	{
		right->parentNode = p;
	}
}
//插入
void insertBTree(BTree * t,keyType k){
	Result r = searchBTree(*t,k);
	if (r.tag ==  0){//不存在 ,插入.
		int index = r.index;//插入位置
		BTree p = r.p;//插入节点
		BTree q = NULL;//插入关键字的 右孩子.
		BTree r = NULL;
		bool finished = false;
		while (p != NULL){
			insert(p,index,k,q);
			if (p->keyNum < M){//M阶 的 树,最多 有 M-1个 关键字{
				finished = true;
				break;
			}
			else{//关键字个数 超过M-1个,需要 分裂.
				int mid = (int)ceil(M/2.0);
				split(p,mid,&q);
				k = p->key[mid];//分裂需要插入 到父节点的关键字
				r = p;
				p = p->parentNode;//父节点
				index = search(p,k);//插入到父节点的位置.
			}
		}
		if (finished == false){//空树,或者 根节点的 关键字 大于 M-1
			newRoot(t,k,r,q);
		}
	}
}

//index之后的关键字 和指针左移
void leftMove(BTree p,int index){
	if(index < p->keyNum){
		for (int i = index; i < p->keyNum; i++){
			if (i>= 1){
				p->key[i] = p->key[i+1];
			}
			p->ptr[i] = p->ptr[i+1];
		}
	}
	else{//在最后一个位置,不需要移动
		p->ptr[p->keyNum] = NULL;
	}
	p->keyNum--;
}

//*t 树根,start:搜索开始的节点
bool deleteBTree(BTree * t,BTree start,keyType key){
	Result r = searchBTree(start,key);
	if (r.tag == 1){//找到了
		BTree p = r.p;
		int index = r.index;
		//不是 最底下一层非终端节点,寻找右子树最小节点,替换k值,并删除最小节点
		if (p->ptr[0] != NULL){
			BTree q = p->ptr[index];
			while (q->ptr[0] != NULL){
				q = q->ptr[0];
			}
			p->key[index] = q->key[1];
			return deleteBTree(t,p->ptr[index],q->key[1]);//删除最小节点退出函数
		}
		else// 是 最底下一层 非终端节点.
		{
			//节点最小阶数为 M/2 向上取整,关键字 比 最小阶数 小1
			int min = (int)ceil(M/2.0)-1;
			BTree parent = p->parentNode;
			//是根节点 或者 节点数 大于 min
			if (p->keyNum > min || parent == NULL ){
				leftMove(p,index);//index后面的关键字左移
				if (p->keyNum == 0){//根节点关键字最少为1,空树.
					free(*t);
					*t = NULL;
				}
				return true;
			}
			else if(p->keyNum == min){
				leftMove(p,index);//将删除关键字后面的 左移
				BTree leftBrother = NULL;
				BTree rightBrother = NULL;
				int pIndex = search(parent,key);//寻找节点在 父节点的 位置(查找位置
				if (pIndex>=1){
					leftBrother = parent->ptr[pIndex-1];//左相邻兄弟
				}
				if (pIndex < parent->keyNum){
					rightBrother = parent->ptr[pIndex+1];//右兄弟
				}
				//寻找 是否 有相邻 兄弟 keyNum大于 min的.
				if(leftBrother != NULL && leftBrother->keyNum > min){//左兄弟有空间
					//整个空间 的关键字 右移一个位置
					for (int i = p->keyNum; i >= 1; i--){
						p->key[i+1] = p->key[i];
					}
					p->key[1] = parent->key[pIndex];//父节点关键字下移
					p->keyNum++;
					parent->key[pIndex] = leftBrother->key[leftBrother->keyNum];//将最大值上移到父节点。
					leftBrother->keyNum--;//最终 左兄弟节点的 关键字少了一个,其余没变.
					return true;
				}
				else if (rightBrother != NULL && rightBrother->keyNum > min){//右兄弟有空间
					p->key[p->keyNum+1] = parent->key[pIndex+1];//父节点关键字下移
					p->keyNum++;
					parent->key[pIndex+1] = rightBrother->key[1];//将最小值上移
					leftMove(rightBrother,1);//将 最小值 之后的 左移.
					return true;
				}
				BTree merger = NULL;//合并后的节点
				while (parent != NULL){
					//左右相邻空间都等于 min,需要 删除节点后合并(书中第三种情况)
					if (leftBrother != NULL){
						merger = leftBrother;
						//将节点中其他关键字 和 父节点的第pindex个 复制到左兄弟中
						int lIndex = leftBrother->keyNum+1;//左兄弟下标
						leftBrother->key[lIndex] = parent->key[pIndex];//先复制父节点关键字
						leftMove(parent,pIndex);//父节点关键字后面的左移
						for (int i = 0; i <= p->keyNum; i++){
							if  (i >=1){
								leftBrother->key[lIndex] = p->key[i];
							}
							leftBrother->ptr[lIndex++] = p->ptr[i];
							if (p->ptr[i] != NULL){//设置新的父节点
								p->ptr[i]->parentNode = leftBrother;
							}
						}
						leftBrother->keyNum += p->keyNum + 1;
						free(p);//合并完成删除节点
					}
					else if (rightBrother != NULL){
						merger = rightBrother;
						//加入到右兄弟节点的关键字个数,减去一个关键字,再加上一个父节点关键字
						int addNum = p->keyNum + 1;
						rightBrother->keyNum += addNum;
						//为插入关键字移位
						for (int i = rightBrother->keyNum; i >= 0; i--){
							if (i >= 1){
								rightBrother->key[i+addNum] = rightBrother->key[i];
							}
							rightBrother->ptr[i+addNum] = rightBrother->ptr[i];
						}
						int i = 0;
						for (; i <= p->keyNum; i++){
							if (i >= 1){
								rightBrother->key[i] = p->key[i];
							}
							rightBrother->ptr[i] = p->ptr[i];
							if (p->ptr[i] != NULL){//设置新的父节点
								p->ptr[i]->parentNode = rightBrother;
							}
						}
						rightBrother->key[i] = parent->key[pIndex+1];
						leftMove(parent,pIndex+1);//左移
						free(p);//合并完成,删除节点
					}
					//父节点关键字不足,继续删除父节点,然后合并.
					if (parent->keyNum < min){
						p = parent;
						parent = parent->parentNode;
						pIndex = search(parent,key);//寻找节点在 父节点的 位置(查找位置
						leftBrother = rightBrother = NULL;
						if (pIndex>=1){
							leftBrother = parent->ptr[pIndex-1];//左相邻兄弟
						}
						if (pIndex < parent->keyNum){
							rightBrother = parent->ptr[pIndex+1];//右兄弟
						}
					}
					else if (parent->keyNum > M-1){//合并后的关键字超过 M-1,需要分裂
						BTree newNode = NULL,r = NULL;
						bool finished = false;
						p = parent;
						keyType k = 0;
						while (p != NULL){
							int mid = (int)ceil(M/2.0);
							split(p,mid,&newNode);
							k = p->key[mid];//分裂需要插入 到父节点的关键字
							r = p;
							p = p->parentNode;
							if (p != NULL){
								index = search(p,k);//插入到父节点的位置.
								insert(p,index,k,newNode);
								if (p->keyNum < M){//M阶 的 树,最多 有 M-1个 关键字{
									finished = true;
									break;
								}
							}
						}
						if (finished == false){//空树,或者 根节点的 关键字 大于 M-1
							newRoot(t,k,r,newNode);
						}
						return true;
					}
				}
				//根节点最少一个,当为0时,将合并的节点设置为新的 根节点。
				if (p->keyNum == 0){
					free(p);
					*t = merger;
					return true;
				}
			}
			else{//error
				printf("------------ERROR--------------\n");
			}
		}
	}
	return false;//没找到
}

//层序遍历
void levelOrderTraverse(BTree tree){
	if (tree != NULL){
		LinkQueue queue;
		queueInit(&queue);
		enqueue(&queue,tree);
		int count = 1;
		int nextCount = 0;
		int level = 1;
		printf("-----------层序遍历--------------\n");
		while (!queueEmpty(queue)){
			printf("第%d层数据:",level);
			for (int i = 0; i < count; i++){
				BTree front;
				dequeue(&queue,&front);
				for (int j= 0; j <= front->keyNum; j++){
					if (j >=1){
						printf("%d\t",front->key[j]);
					}
					if (front->ptr[j] != NULL){
						enqueue(&queue,front->ptr[j]);
					}
					nextCount++;
				}
				printf("------");
			}
			printf("\n");
			count = nextCount;
			nextCount = 0;
			level ++;
		}
		queueDestory(&queue);
		printf("\n");
	}
}

int _tmain(int argc, _TCHAR* argv[])
{
	BTree tree;
	initTree(&tree);
	int array [11] = {22,11,55,66,77,33,44,99,88,22,33};
	for (int i = 0; i < 11; i++){
		insertBTree(&tree,array[i]);
		//printf("-----插入%d后------------\n",array[i]);
		//levelOrderTraverse(tree);
	}
	levelOrderTraverse(tree);
	return 0;
}

其中包含了部分不正确的 B-树 删除代码,在处理 下面 这个问题时,不正确。书中 也没提到 这种情况。希望 以后 完善。

时间: 2024-07-31 04:04:35

看数据结构写代码(58) B-树的相关文章

看数据结构写代码(32) 赫夫曼树编码以及译码

杂谈:最近有点慵懒,不好不好.好几天都没写代码,原本准备上星期完结 树 这一章节的.现在 又耽误了.哎.要抓紧时间啊. 下面直接上代码: 可以到我的网盘下载源代码,或者 直接拷贝下面的源代码 运行 网盘地址:点击打开链接 // HuffmanTree.cpp : 定义控制台应用程序的入口点. //哈弗曼编码,译码 #include "stdafx.h" #include <stdlib.h> #include <cstring> enum E_State { E

看数据结构写代码(57) AVL树的删除

上一节 已经说了 AVL树的插入 操作,可是 只有 插入,没有删除,怎么能叫 动态 查找表呢. 呵呵,博主 赶紧 去 研究了一番.下面 是成果: AVL树的删除 大致 分为 两大块: 1. 查找节点 并 删除 2. 保持 删除 后 平衡因子的 影响 1. 首先 找到 这个 节点,如果 节点 不存在,直接 退出 函数 if (*tree == NULL){//没找到 return false; } 2.如果 存在,分为 四种情况:(根 二叉 排序树的 删除 类似) 1.节点 为 叶子 节点,直接

看数据结构写代码(66) 败者树

计算机的 内存 是 有限的,无法 存入 庞大的数据.当 遇到 大数据需要排序时,我们 需要 将 这些 数据 分段 从 硬盘里 读到 内存中,排好序,再 写入到 硬盘中,这些段 叫做 归并段.最后将 这些 分段 合并 成 一个 最终  完整 有序的 数据. 这里 操作的 时间 =  内部 排序 时间 +  外存读写时间 + 内部归并所需时间. 其中 外存 读写时间 最耗时,外存读写时间 = 读写次数 * 读写数据的时间 ,读写 数据的时间 因 设备 性能 而 影响,我们 无法控制,我们 只能 控制

看数据结构写代码(59) 键树的双链表示法

杂谈; 打败自己的 往往不是敌人,而是自己.坚持不易,且行且珍惜. 键树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种.典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计.它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高 键树的根节点不包含字符,除根节点外的非叶子节点都只包含一个字符,叶子节点存储具体信息.: 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串:

看数据结构写代码(54)次优查找树

查找顺序表时,若 每个元素的概率 都相等 用 二分查找 效率 最高.但是 如果 概率 不相等时,(SOST)静态最优查找表 效率 要高于 二分查找.静态最优查找表 是 使得 从 根 到 每个节点的路径 长度 和 权值 乘积 之和 最小. 书上说的 静态最优 查找树的创建 时间 复杂度 较高,所以 用 次优 查找树(NOST) 代替. 下面 上代码: // Nost.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <cstd

看数据结构写代码(34) 树与回溯法(二)排序树(8皇后问题)

套用回溯 公式程序: void backtrack (int t) { if (t > n) { // 到达叶子结点,将结果输出 output (x); } else { // 遍历结点t的所有子结点 for (int i = f(n,t); i <= g(n,t); i ++ ) { x[t] = h[i]; // 如果不满足剪枝条件,则继续遍历 if (constraint (t) && bound (t)) backtrack (t + 1); } } } 写出 8皇后问

看数据结构写代码(60 ) 键树的多重链表表示(Trie树)

trie树,是用 树的 多重链表来表示 树的.每个节点 有 d 个指针域.若从键树中的某个节点到叶子节点的路径上每个节点都只有一个孩子,则可以把 路径上的所有节点压缩成一个叶子节点,且在叶子节点中 存储 关键字 以及 根关键字相关的信息. 当节点的度 比较大时,选择 Trie树,要比 双链表树更为合适. tire树的 数据 压缩 是 挺与众不同的. 下面 给出 具体的 代码: 源代码工程文件网盘地址:http://pan.baidu.com/s/1cyTg6 // TrieTree.cpp :

看数据结构写代码(33) 树与回溯法(一) 子集树

回溯法 是 一种 在 穷举 中,裁剪 不满足 条件 的 分支,已达到 提高 效率的 方法.其基本原型 是 树的 先序遍历,从 树根 到 树叶的路径 是 问题的 一个 解. 回溯法的基本框架 =  确定 解空间 + 深度优先遍历 + 裁剪函数 + 确定结果函数 其中 解空间,分为 子集树 和 排序树. 具体 概念 详解:参考 点击打开链接  和 点击打开链接 递归算法通用 模板如下: 回溯法对解空间作深度优先搜索,因此,在一般情况下用递归方法实现回溯法. // 针对N叉树的递归回溯方法 void

看数据结构写代码(67) 置换 _ 选择排序(完结篇)

杂谈: 严蔚敏版<数据结构(C语言版)> 一书 终于看完了.这是 一个完结,也是 一个新的开端.<算法导论> 已到手. 置换选择排序的思想 是 将 归并段 尽量 变的 更大,而不是根据 内存 大小 限制在 固定的 大小. 这样 可以 利用赫夫曼树 来 进行 最优归并树,从而 使 外存 读写次数 最少. 下面给出 具体 代码:欢迎指出代码不足. // Replace_Selcetion.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h&q