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树的存储结构
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 =