我在大二上学期的数据结构实验设计中选择了B树这个题目,该B树的数据结构实现采用了C语言。趁现在寒假整理完写一篇博文记录我的学习。文末提供了项目源代码的地址。
B树的定义
一棵m阶B树(Balanced Tree of order m),或为空树,或为满足下列特性对的m叉树。
- 树中每个结点最多含有m棵子树。
- 若根结点不是叶子结点,则至少有2个子树。
- 除根结点之外的所有非终端结点至少有 ⌈m/2⌉ ⌈ m / 2 ⌉ 棵子树。
- 每个非终端结点中包含信息:( n,A0,K1,A1,K2,A2,…,Kn,An n , A 0 , K 1 , A 1 , K 2 , A 2 , … , K n , A n )。其中:
- Ki(1≤i≤n) K i ( 1 ≤ i ≤ n ) 为关键字,且关键字按升序排序。
- 指针 Ai(0≤i≤n) A i ( 0 ≤ i ≤ n ) 指向子树的根结点, Ai−1 A i − 1 指向子树中所有结点的关键字均小于 Ki K i ,且大于 Ki−1 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树的查找从根结点开始,然后重复以下过程:
- 若给定关键字等于结点中某个关键字 Ki K i ,则查找成功。
- 若给定关键字比结点中的 K1 K 1 小,则进入指针 A0 A 0 指向的下一层结点继续查找。
- 若比该结点所有关键字大,则在其最后一个指针 An A n 指向的下一层结点继续寻找;
- 若查找到叶子结点,则说明给定值对应的数据记录不存在,查找失败。
实现代码
/** * 在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树的插入首先利用了B树的查找操作查找关键字k的插入位置。若该关键字存在于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 K i 一起,合并到 Ai−1 A i − 1 或( Ai A i )结点,即删除该关键字结点的左(右)兄弟结点。如果导致双亲结点中关键字个数小于 ⌈m/2⌉−1 ⌈ m / 2 ⌉ − 1 ,则对此双亲结点做同样处理。如果知道根结点也做了合并,则整棵树减少一层。如下图
该结点不是最下层非终端结点
假设被删关键字为该结点中第i个关键字 Ki K i ,则可从指针 Ai A i 所指的子树中找出位于最下层非终端结点的最小关键字替代 Ki K i ,并将其删除,即可转换为上面的情况进行操作。
代码实现如下
/** * 删除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。