B树之C语言实现(包含查找、删除、插入)
B树的定义
一棵m阶B树(Balanced Tree of order m),或为空树,或为满足下列特性对的m叉树。
- 树中每个结点最多含有m棵子树。
- 若根结点不是叶子结点,则至少有2个子树。
- 除根结点之外的所有非终端结点至少有⌈m/2⌉⌈m/2⌉棵子树。
- 每个非终端结点中包含信息:(n,A0,K1,A1,K2,A2,…,Kn,Ann,A0,K1,A1,K2,A2,…,Kn,An)。其中:
- Ki(1≤i≤n)Ki(1≤i≤n)为关键字,且关键字按升序排序。
- 指针$Ai(0\leq i\leq n)指向子树的根结点,指向子树的根结点,A{i-1}指向子树中所有结点的关键字均小于指向子树中所有结点的关键字均小于Ki,且大于,且大于K{i-1}$;
- 关键字的个数n必须满足:⌈m/2⌉−1≤n≤m−1⌈m/2⌉−1≤n≤m−1。
- 所有的叶子节点都在同一层,子叶结点不包含任何信息。
B树的存储结构
- #define m 3 // B树的阶,此设为3
typedef int KeyType;- typedef struct {
KeyType key;
char data;
} Record;- typedef struct BTNode {
int keynum; // 结点中关键字个数,即结点的大小
struct BTNode *parent; // 指向双亲结点
KeyType key[m+1]; // 关键字向量,0号单元未用
struct BTNode *ptr[m+1]; // 子树指针向量
Record *recptr[m+1]; // 记录指针向量,0号单元未用
} BTNode, *BTree; // B树结点和B树的类型- typedef struct {
BTree pt; // 指向找到的结点
int i; // 1..m,在结点中的关键字序号
int tag; // 1:查找成功,0:查找失败
} Result; // 在B树的查找结果类型
B树的查找从根结点开始,然后重复以下过程:
B树的查找
- 若给定关键字等于结点中某个关键字KiKi,则查找成功。
- 若给定关键字比结点中的K1K1小,则进入指针A0A0指向的下一层结点继续查找。
- 若比该结点所有关键字大,则在其最后一个指针AnAn指向的下一层结点继续寻找;
- 若查找到叶子结点,则说明给定值对应的数据记录不存在,查找失败。
实现代码
- /**
* 在B-树t查找关键字key,用r返回(pt, i, tag)
* 若查找成功,则tag=1,指针pt所指结点中第i个关键字等于key
* 否则tag=0,若要插入关键字key,应位于pt结点中第i-1和第i个关键字之间
*
* @param t B-树
* @param key 待查找的关键字
* @param r B-树的查找结果类型
*/
void SearchBTree(BTree t, KeyType key, Result &r) {
int i = 0;
int found = 0;
BTree p = t; // 一开始指向根结点,之后指向待查结点
BTree q = NULL; // 指向待查结点的双亲
while (p != NULL && found == 0) {
i = Search(p, key);
if (i <= p->keynum && p->key[i] == key) {
found = 1;
} else {
q = p;
p = p->ptr[i - 1]; // 指针下移
}
}
if (1 == found) { // 查找成功,返回key的位置p和i
r.pt = p;
r.i = i;
r.tag = 1;
} else { // 查找失败,返回key的插入位置q和i
r.pt = q;
r.i = i;
r.tag = 0;
}
}- /**
* 在p->key[1 .. p->keynum]找key,并返回位序
*
* @param p B-树的结点p
* @param key 关键字
* @return key在p结点的位序
*/
int Search(BTree p, KeyType key) {
int i = 1;
while (i <= p->keynum && key > p->key[i]) {
i++;
}
return i;
}
B树的插入首先利用了B树的查找操作查找关键字k的插入位置。若该关键字存在于B树中,则不用插入直接返回,否则查找操作必定失败于某个最底层的非终端结点上,也就是要插入的位置。插入分两种情况讨论。
B树的插入
- 插入关键字后该结点的关键字数小于等于 m - 1,插入操作结束。
- 插入关键字后该结点的关键字数等于m,则应该进行分裂操作,分裂操作如下:
- 生成一个新结点,从中间位置把结点(不包括中间位置的关键字)分为两部分。
- 前半部分留在旧结点中,后半部分复制到新结点中。
- 中间位置的关键字连同新结点的存储位置插入到父结点中,如果插入后父结点的关键字个数也超过了m-1,则继续分裂。
下图是插入的示意图(因为不想画图,而且这个好像也没有那么容易画,所以就很不要脸的拍了课本的照片)
下面给出插入代码的实现
- /**
* 在B-树t中q结点的key[i - 1]和key[i]之间插入关键字key
* 若插入后结点关键字个数等于B-树的阶,则沿双亲指针链进行结点分裂
*
* @param t B-树
* @param key 待插入的关键字
* @param q 关键字插入的结点
* @param i 插入位序
*/
void InsertBTree(BTree &t, KeyType key, BTree q, int i) {
KeyType x;
int s;
int finished = FALSE;
int needNewRoot = FALSE;
BTree ap;
if (NULL == q) {
newRoot(t, NULL, key, NULL);
} else {
x = key;
ap = NULL;
while (FALSE == needNewRoot && FALSE == finished) {
Insert(q, i, x, ap); // x和ap分别插入到q->key[i]和q->ptr[i]
if (q->keynum < m) {
finished = TRUE;
} else {
// 分裂q结点
s = (m + 1) / 2; // 得到中间结点位置
split(q, s, ap);
x = q->key[s];
// 在双亲位置插入关键字x
if (q->parent != NULL) {
q = q->parent;
i = Search(q, x); // 寻找插入的位置
} else {
needNewRoot = TRUE;
}
}
}
if (TRUE == needNewRoot) {
newRoot(t, q, x, ap);
}
}
}- /**
* 将q结点分裂成两个结点,前一半保留在原结点,后一半移入ap所指新结点
*
* @param q B-树结点
* @param s 中间位序
* @param ap 新结点,用来存放原结点的后一半关键字
*/
void split(BTree &q, int s, BTree &ap) {
int i, j;
int n = q->keynum; // 关键字数量
ap = (BTree)malloc(sizeof(BTNode));
ap->ptr[0] = q->ptr[s];
for (i = s + 1, j = 1; i <= n; i++, j++) {
ap->key[j] = q->key[i];
ap->ptr[j] = q->ptr[i];
}
ap->keynum = n - s;
ap->parent = q->parent;
for (i = 0; i <= n - s; i++) {
// 修改新结点的子结点的parent域
if (ap->ptr[i] != NULL) {
ap->ptr[i]->parent = ap;
}
}
q->keynum = s - 1; // 修改q结点的关键字数量
}- /**
* 生成新的根结点
*
* @param t B-树
* @param p B-树结点
* @param key 关键字
* @param ap B-树结点
*/
void newRoot(BTree &t, BTree p, KeyType key, BTree ap) {
t = (BTree)malloc(sizeof(BTNode));
t->keynum = 1;
t->ptr[0] = p;
t->ptr[1] = ap;
t->key[1] = key;
if (p != NULL) {
p->parent = t;
}
if (ap != NULL) {
ap->parent = t;
}
t->parent = NULL;
}- /**
* 关键字key和新结点指针ap分别插入到q->key[i]和q->ptr[i]
*
* @param q 插入目标结点
* @param i 插入位序
* @param key 待插入的关键字
* @param ap 新结点指针
*/
void Insert(BTree &q, int i, KeyType key, BTree ap) {
int j;
int n = q->keynum;
for (j = n; j >= i; j--) {
// 后移
q->key[j + 1] = q->key[j];
q->ptr[j + 1] = q->ptr[j];
}
q->key[i] = key;
q->ptr[i] = ap;
if (ap != NULL) {
ap->parent = q;
}
q->keynum++;
}
B树的删除关键字是B树所有操作种最麻烦的,下面一一讲解。
B树的删除
该结点为最下层非终端结点
- 如果被删关键字所在结点的原关键字个数n≥⌈m/2⌉n≥⌈m/2⌉,则删去该关键字后结点仍满足B树的定义,如下图
- 如果被删关键字所在结点的关键字个数n等于⌈m/2⌉−1⌈m/2⌉−1,则删除该关键字后该结点将不满足B树的定义,需要调整:如果其左右兄弟结点中有富余的关键字,即与该结点相邻的右(或左)兄弟结点中的关键字数目大于⌈m/2⌉−1⌈m/2⌉−1,则可将右(或左)兄弟结点中最小(大)关键字上移至双亲结点。而将双亲结点中小(大)于该上移关键字的关键字下移至被删关键字所在结点中。如下图
- 如果左右兄弟结点都没有多余的关键字,则需要把要删除关键字的结点与其左(或右)兄弟结点以及双亲结点中分割两者的关键字合并成一个结点,即在删除关键字后,该结点中剩余的关键字和指针,加上双亲结点中的关键字$Ki一起,合并到一起,合并到A{i-1}或(或(A_i)结点,即删除该关键字结点的左(右)兄弟结点。如果导致双亲结点中关键字个数小于)结点,即删除该关键字结点的左(右)兄弟结点。如果导致双亲结点中关键字个数小于\lceil m/2\rceil-1$,则对此双亲结点做同样处理。如果知道根结点也做了合并,则整棵树减少一层。如下图
该结点不是最下层非终端结点
假设被删关键字为该结点中第i个关键字KiKi,则可从指针AiAi所指的子树中找出位于最下层非终端结点的最小关键字替代KiKi,并将其删除,即可转换为上面的情况进行操作。
代码实现如下
- /**
* 删除B-树上p结点的第i个关键字
*
* @param t B-树
* @param p 目标关键字所在结点
* @param i 关键字位序
*/
void DeleteBTree(BTree &t, BTree &p, int i) {
if (p->ptr[i] != NULL) {
// 不是最下层非终端结点
Successor(p, i); // 找到后继最下层非终端结点的最小关键字代替它
DeleteBTree(t, p, 1); // 删除最下层非终端结点中的最小关键字
} else {
Remove(p, i); // 从结点p中删除key[i]
if (p->keynum < (m - 1) / 2) {
Restore(t, p); // 调整B树
}
}
}- /**
* 在Ai子树中找出最下层非终端结点的最小关键字代替Ki
*
* @param p B-树结点
* @param i 关键字位序
*/
void Successor(BTree &p, int i) {
BTree leaf = p;
if (NULL == p) {
return;
}
leaf = leaf->ptr[i]; // 指向子树
while (NULL != leaf->ptr[0]) {
// 找到最下层非终端结点
leaf = leaf->ptr[0];
}
p->key[i] = leaf->key[1];
p = leaf;
}- /**
* 从结点p移除关键字key[i]
*
* @param p B-树结点
* @param i 关键字位序
*/
void Remove(BTree &p, int i) {
int k;
// 指针与key都向左移
for (k = i; k < p->keynum; k++) {
p->key[k] = p->key[k + 1];
p->ptr[k] = p->ptr[k + 1];
}
p->keynum--;
}- /**
* 调整B-树
*
* @param t B-树
* @param p B-树结点
*/
void Restore(BTree &t, BTree &p) {
BTree parent, leftBrother, rightBrother; // 被删结点的父结点、左右兄弟
parent = p->parent;
if (parent != NULL) { // 父结点不为空
// 寻找左右兄弟
int i;
for (i = 0; i <= parent->keynum; i++) {
if (parent->ptr[i] == p) {
break;
}
}
if (i > 0) {
leftBrother = parent->ptr[i - 1];
} else {
leftBrother = NULL;
}
if (i < parent->keynum) {
rightBrother = parent->ptr[i + 1];
} else {
rightBrother = NULL;
}- // 左兄弟或右兄弟有富余关键字
if ((leftBrother != NULL && leftBrother->keynum >= (m + 1) / 2) ||
(rightBrother != NULL && rightBrother->keynum >= (m + 1) / 2)) {
BorrowFromBrother(p, leftBrother, rightBrother, parent, i);
} else {
// 左右兄弟都没富余关键字,需要合并
if (leftBrother != NULL) {
MegerWithLeftBrother(leftBrother, parent, p, t, i); // 与左兄弟合并
} else if (rightBrother != NULL) {
MegerWithRightBrother(rightBrother, parent, p, t, i);
} else {
//当左右子树不存在时改变根结点
for (int j = 0; j <= p->keynum + 1; j++) {
if (p->ptr[j] != NULL) {
t = p->ptr[j];
break;
}
}
t->parent = NULL;
}
}
} else {
//根节点,去掉根节点,使树减一层
BTree a;
for (int j = 0; j <= p->keynum + 1; j++) {
if (p->ptr[j] != NULL) {
a = p;
p = p->ptr[j];
a->ptr[j] = NULL;
free(a);
break;
}
}
t = p;
t->parent = NULL;
}
}- /**
* 向兄弟借关键字
*
* @param p B-树结点
* @param leftBrother p结点的左兄弟结点
* @param rightBrother p结点的右兄弟结点
* @param parent p结点的父亲结点
* @param i 位序
*/
void BorrowFromBrother(BTree &p, BTree &leftBrother, BTree &rightBrother, BTree &parent, int &i) {
// 左兄弟有富余关键字,向左兄弟借
if (leftBrother != NULL && leftBrother->keynum >= (m + 1) / 2) {
for (int j = p->keynum + 1; j > 0; j--) {
// 关键字与指针后移,腾出第一个位置
if (j > 1) {
p->key[j] = p->key[j - 1];
}
p->ptr[j] = p->ptr[j - 1];
}
p->ptr[0] = leftBrother->ptr[leftBrother->keynum];
if (p->ptr[0] != NULL) {
p->ptr[0]->parent = p;
}
leftBrother->ptr[leftBrother->keynum] = NULL;
p->key[1] = parent->key[i]; // 被删结点存父结点关键字
parent->key[i] = leftBrother->key[leftBrother->keynum]; // 父结点的key变为被删结点左兄弟的最大关键字
leftBrother->keynum--;
p->keynum++;
} else if (rightBrother != NULL && rightBrother->keynum >= (m + 1) / 2) { // 右兄弟有富余关键字
p->key[p->keynum + 1] = parent->key[i + 1];
p->ptr[p->keynum + 1] = rightBrother->ptr[0]; // 子树指针指向右兄弟最左边的子树指针
if (p->ptr[p->keynum + 1] != NULL) {
p->ptr[p->keynum + 1]->parent = p;
}
p->keynum++;
parent->key[i + 1] = rightBrother->key[1]; // 父结点从右兄弟借关键字
for (int j = 0; j < rightBrother->keynum; j++) {
if (j > 0) {
rightBrother->key[j] = rightBrother->key[j + 1];
}
rightBrother->ptr[j] = rightBrother->ptr[j + 1];
}
rightBrother->ptr[rightBrother->keynum] = NULL;
rightBrother->keynum--;
}
}- /**
* 与左兄弟合并
*
* @param leftBrother p结点的左兄弟结点
* @param parent p结点的父亲结点
* @param p B-树结点
* @param t B-树
* @param i 位序
*/
void MegerWithLeftBrother(BTree &leftBrother, BTree &parent, BTree &p, BTree &t, int &i) {
// 与左兄弟合并
leftBrother->key[leftBrother->keynum + 1] = parent->key[i]; // 从父结点拿下分割本节点与左兄弟的关键字
leftBrother->ptr[leftBrother->keynum + 1] = p->ptr[0];
if (leftBrother->ptr[leftBrother->keynum + 1] != NULL) {
leftBrother->ptr[leftBrother->keynum + 1]->parent = leftBrother; // 给左兄弟的结点,当此结点存在时需要把其父亲指向指向左结点
}
leftBrother->keynum++; //左兄弟关键数加1
for (int j = 1; j <= p->keynum; j++) {
// 把本结点的关键字和子树指针赋给左兄弟
leftBrother->key[leftBrother->keynum + j] = p->key[j];
leftBrother->ptr[leftBrother->keynum + j] = p->ptr[j];
if (leftBrother->ptr[leftBrother->keynum + j] != NULL) {
leftBrother->ptr[leftBrother->keynum + j]->parent = leftBrother;
}
}
leftBrother->keynum += p->keynum;
parent->ptr[i] = NULL;
free(p); // 释放p结点
for (int j = i;j < parent->keynum; j++) {
// 左移
parent->key[j] = parent->key[j + 1];
parent->ptr[j] = parent->ptr[j + 1];
}
parent->ptr[parent->keynum] = NULL;
parent->keynum--; // 父结点关键字个数减1
if (t == parent) {
// 如果此时父结点为根,则当父结点没有关键字时才调整
if (0 == parent->keynum) {
for (int j = 0;j <= parent->keynum + 1; j++) {
if (parent->ptr[j] != NULL) {
t = parent->ptr[j];
break;
}
t->parent = NULL;
}
}
} else {
// 如果父结点不为根,则需要判断是否需要重新调整
if (parent->keynum < (m - 1) / 2) {
Restore(t, parent);
}
}
}
/**
* 与右兄弟合并
*
* @param rightBrother p结点的右兄弟结点
* @param parent p结点的父亲结点
* @param p B-树结点
* @param t B-树
* @param i 位序
*/
void MegerWithRightBrother(BTree &rightBrother, BTree &parent, BTree &p, BTree &t, int &i) {
// 与右兄弟合并
for (int j = (rightBrother->keynum); j > 0; j--) {
if (j > 0) {
rightBrother->key[j + 1 + p->keynum] = rightBrother->key[j];
}
rightBrother->ptr[j + 1 + p->keynum] = rightBrother->ptr[j];
}
rightBrother->key[p->keynum + 1] = parent->key[i + 1]; // 把父结点的分割两个本兄弟和右兄弟的关键字拿下来使用
for (int j = 0; j <= p->keynum; j++) {
// 把本结点的关键字及子树指针移动右兄弟中去
if (j > 0) {
rightBrother->key[j] = p->key[j];
}
rightBrother->ptr[j] = p->ptr[j];
if (rightBrother->ptr[j] != NULL) {
rightBrother->ptr[j]->parent = rightBrother; // 给右兄弟的结点需要把其父结点指向右兄弟
}
}
rightBrother->keynum += (p->keynum + 1);
parent->ptr[i] = NULL;
free(p); // 释放p结点
for (int j = i;j < parent->keynum;j++) {
if (j > i) {
parent->key[j] = parent->key[j + 1];
}
parent->ptr[j] = parent->ptr[j + 1];
}
if (1 == parent->keynum) {
// 如果父结点在关键字减少之前只有一个结点,那么需要把父结点的右孩子赋值给左孩子
parent->ptr[0] = parent->ptr[1];
}
parent->ptr[parent->keynum] = NULL;
parent->keynum--; // 父结点关键字数减1
if (t == parent) {
//如果此时父结点为根,则当父结点没有关键字时才调整
if (0 == parent->keynum) {
for (int j = 0; j <= parent->keynum + 1; j++) {
if (parent->ptr[j] != NULL) {
t = parent->ptr[j];
break;
}
}
t->parent = NULL;
}
} else {
//如果父结点不为根,则需要判断是否需要重新调整
if (parent->keynum < (m - 1) / 2) {
Restore(t, parent);
}
}
}
如需要完整的实现代码以及测试函数,请到https://github.com/zhengjunming/ds/tree/master/btree 中clone
C語言版:
https://blog.csdn.net/xiaohusaier/article/details/76708490
B树之C语言实现(包含查找、删除、插入)的更多相关文章
- 二叉搜索树Java实现(查找、插入、删除、遍历)
由于最近想要阅读下 JDK1.8 中 HashMap 的具体实现,但是由于 HashMap 的实现中用到了红黑树,所以我觉得有必要先复习下红黑树的相关知识,所以写下这篇随笔备忘,有不对的地方请指出- ...
- AVL树(查找、插入、删除)——C语言
AVL树 平衡二叉查找树(Self-balancing binary search tree)又被称为AVL树(AVL树是根据它的发明者G. M. Adelson-Velskii和E. M. Land ...
- 浅谈算法和数据结构: 七 二叉查找树 八 平衡查找树之2-3树 九 平衡查找树之红黑树 十 平衡查找树之B树
http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 前文介绍了符号表的两种实现,无序链表和有序数组,无序链表在插入的 ...
- 二叉查找树(查找、插入、删除)——C语言
二叉查找树 二叉查找树(BST:Binary Search Tree)是一种特殊的二叉树,它改善了二叉树节点查找的效率.二叉查找树有以下性质: (1)若左子树不空,则左子树上所有节点的值均小于它的根节 ...
- 单链表创建、删除、查找、插入之C语言实现
本文将详细的介绍C语言单链表的创建.删除.查找.插入以及输出功能 一.创建 #include<stdio.h> #include<stdlib.h> typedef int E ...
- c语言文件包含
文件包含是指一个C语言源程序中将另一个C语言源程序包含进来,通过include预处理指令实现. 一般形式: #include”被包含文件名” 或#include<被包含文件名> 2. 作 ...
- 二叉查找树的查找、插入和删除 - Java实现
http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 作者: yangecnu(yangecnu's Blog on ...
- C语言习题 链表建立,插入,删除,输出
Problem B: C语言习题 链表建立,插入,删除,输出 Time Limit: 1 Sec Memory Limit: 128 MB Submit: 222 Solved: 92 [Subm ...
- 算法与数据结构(十) 二叉排序树的查找、插入与删除(Swift版)
在上一篇博客中,我们主要介绍了四种查找的方法,包括顺序查找.折半查找.插入查找以及Fibonacci查找.上面这几种查找方式都是基于线性表的查找方式,今天博客中我们来介绍一下基于二叉树结构的查找,也就 ...
- YTU 2430: C语言习题 链表建立,插入,删除,输出
2430: C语言习题 链表建立,插入,删除,输出 时间限制: 1 Sec 内存限制: 128 MB 提交: 576 解决: 280 题目描述 编写一个函数creatlink,用来建立一个动态链表 ...
随机推荐
- Vue Loader
介绍 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug(模板 ...
- leetcode75
class Solution { public: void sortColors(vector<int>& nums) { sort(nums.begin(), nums.end( ...
- Redux-persist使用
redux-persist作用是将store中的数据缓存到浏览器中,减少数据请求,每当白名单中的数据发生变化,才会进行一次更新缓存的操作,并且这个数据缓存是存在localStorage中的,不是会话级 ...
- Flannel网络插件配置
# ps -ef|grep docker|grep bip FLANNEL_OPTIONS="-ip-masq=true" -ip-masq=true 这个参数的目的是让flann ...
- k8s之配置flanneld网络
Flannel是Overlay网络的一种,也是将源数据包封装在另一种网络包里面进行路由转发和通信,目前已经支持UDP.VXLAN.AWS VPC和GCE路由等数据转发方式. Flannel通过给每台宿 ...
- java学习笔记(七):for循环
java的for循环和c++的for循环类似 public class Test { public static void main(String args[]) { for(int x = 10; ...
- 20162322 朱娅霖 作业011 Hash
20162322 2017-2018-1 <程序设计与数据结构>第十一周学习总结 教材学习内容总结 哈希方法 一.定义 哈希:次序--更具体来说是项在集合中的位置--由所保存元素值的某个函 ...
- 863. All Nodes Distance K in Binary Tree 到制定节点距离为k的节点
[抄题]: We are given a binary tree (with root node root), a target node, and an integer value K. Retur ...
- 149. Max Points on a Line同一条线上的最多点数
[抄题]: Given n points on a 2D plane, find the maximum number of points that lie on the same straight ...
- Python项目--Scrapy框架(一)
环境 win8, python3.7, pycharm 正文 1.Scrapy框架的安装 在cmd命令行窗口执行: pip install Scrapy 即可完成Scrapy框架的安装 2. 创建Sc ...