一、实验概要
- 实验项目名称: 抽象数据类型【B 树】的实现
- 实验项目性质: 设计性实验
- 所属课程名称: 数据结构
二、实验目的
对某组具体的抽象数据类型,运用课程所学的知识和方法,设计合理的数据结构,并在此基础上实现该抽象数据类型的全部基本操作。通过本设计性实验,检验所学知识和能力,发现学习中存在的问题。 进而达到熟练地运用本课程中的基础知识及技术的目的。
三、实验编程环境
编程环境:Vs Code
编程语言:C
四、实验要求(题目)
利用 C 语言数据类型表示 B 树的抽象数据类型,以及 B 树的抽象数据类型的实现。
抽象数据类型树的定义:树的结构定义和树的一组基本操作
4.1 B 树数据对象
B 树是一种平衡的多路查找树。
4.2 B 树的数据关系
一颗 m 阶 B 树,或为空树,或为满足下列特性的 m 叉树。
- 树中每个结点最多含有 m 棵子树;
- 若根结点不是叶子结点,则至少有两颗子树;
- 除根之外的所有非终端结点至少有[m/2];
- 每个非终端结点中包含信息:(n,A0,K1,A1,K2,A2,…,Kn,An)。其中
- ①Ki(1≤i≤n)为关键字,且关键字按升序排序。
- ② 指针 Ai(0≤i≤n)指向子树的根结点。
- ③ 关键字的个数 n 必须满足:[m/2]-1≤n≤m-1
- 所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部节点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)
4.3 程序结构图
4.4 B 树的基本操作
void InitBTNode(BTree &p);
初始条件:结点 p 存在。
操作结果:初始化结点。
int InsertBTree(BTree &T, int k, Record rcd);
初始条件:结点 T 存在。
操作结果:在 B 树中插入关键字为 k 的记录 rcd
void split(BTree &q, BTree &ap); //分裂饱和结点
初始条件:结点 q 和结点 ap 已存在。
操作结果:将结点 q 分裂成两个结点,前一半保留,后一半移入结点 ap。
void newRoot(BTree &T, BTree p, BTree ap, int k, Record rcd);
初始条件:结点 T,p,ap 已存在。
操作结果:生成新的根结点 T 原 p 和 ap 为子树指针
void Insert(BTree &q, int k, int index, BTree ap, Record rcd); //插入关键字及指针ap
初始条件:结点 q 和结点 ap 已存在,0<indexkeynum
操作结果:将关键字 k 和结点 ap 分别插入到 q->key[index+1]和 q->ptr[index+1]中
int DeleteBTree(BTree &T, int key); //删除索引为key的记录
初始条件:B 树 T 已存在
操作结果:在 B 树 T 中删除关键字 key
void Successor(BTree &node, int &index);
初始条件:B 树 node 已存在
操作结果:将直接前驱的索引和值覆盖掉当前结点
int Remove(BTree& node, int i);
初始条件:结点 node 已存在,0<ikeynum
操作结果:node 结点删除 key[i]和它的孩子指针 ptr[i]
int InsertRecord(BTree& node, int key,int i, Record rcd);
初始条件:结点 node 已存在,0<ikeynum
操作结果:向父结点插入关键字
void Restore(BTree& node, int index);
初始条件:结点 node 已存在,0<indexkeynum
操作结果:调整 node,使得 B 树正常
void CombineBTNode(BTree& l_node, BTree& r_node);
初始条件:结点 l_node,r_node 已存在
操作结果:将右节点数据调整至左节点,释放右节点
void DeleteRoot(BTree& root);
初始条件:结点 root 已存在
操作结果:将合并后结点中所有记录插入到父节点中
void Traverse(BTree t, int k);
初始条件:B 树 t 已存在
操作结果:遍历 B 树
void PrintBTree(BTree t);
初始条件:B 树 t 已存在
操作结果:遍历打印 B 树
void SearchBTree(BTree T, int k, result &r);
初始条件:结点 T 已存在
操作结果:在结点 T 中查找关键字 k 的插入位置 i,1) 用 r 返回(pt,i,tag)
int Search(BTree p, int k);
初始条件:结点 p 已存在
操作结果:在 p->key[1…p->keynum]中查找 p->key[i-1]<k<=p->key[i]
五、具体实现
5.1 公用头文件
公用头文件 includes.h:
#pragma once
#include <stdio.h>
#include <stdlib.h>
using namespace std;
#define m 4 //B树的阶
#define MAX_NUM m //关键字上限
#define MIN_NUM (m-1)/2 //除头结点外关键字下限
#define SPLIT_INDEX (m+1)/2 //分裂处下标
#define dataType int //关键字数据类型
typedef struct{
dataType data;
}record, *Record; //关键字类型
typedef struct BTNode{
int keynum; //结点当前的关键字个数
int key[m+1]; //索引数组,key[0]不用
struct BTNode* parent; //双亲结点指针
struct BTNode* ptr[m+1]; //孩子结点指针数组
Record recptr[m+1]; //记录指针向量,recptr[0]不用
}BTNode, *BTree;
typedef struct{
int index; //1<=index<=m,在结点中的关键字位序
int tag; //1:查找成功,0:查找失败
BTNode* node; //指向找到的结点
}result;
//insert
void InitBTNode(BTree &p); //初始化结点
int InsertBTree(BTree &T, int k, Record rcd); //插入结点
void split(BTree &q, BTree &ap); //分裂饱和结点
void newRoot(BTree &T, BTree p, BTree ap, int k, Record rcd); //创建新根结点
void Insert(BTree &q, int k, int index, BTree ap, Record rcd); //插入关键字及指针ap
//delete
int DeleteBTree(BTree &T, int key); //删除索引为key的记录
void Successor(BTree &node, int &index); //若不是终端结点,将直接前驱的索引和值覆盖掉当前结点
int Remove(BTree& node, int i); //从结点p中删除key[i]
int InsertRecord(BTree& node, int key,int i, Record rcd); //向父结点插入关键字
void Restore(BTree& node, int index); //调整树
void CombineBTNode(BTree& l_node, BTree& r_node); //合并结点
void DeleteRoot(BTree& root); //将合并后结点中所有记录插入到父节点中
//print
void Traverse(BTree t, int k); //遍历B树
void PrintBTree(BTree t); //打印B树
//search
void SearchBTree(BTree T, int k, result &r); //在m阶B树t上查找索引k,用r返回(pt,i,tag)
int Search(BTree p, int k); //在p->key[1..p->keynum]中查找p->key[i-1]<k<=p->key[i]
5.2 算法设计
void InitBTNode(BTree &p);
初始条件:结点 p 存在。
操作结果:初始化结点。
void InitBTNode(BTree &p){
p = (BTree)malloc(sizeof(BTNode));
//对p的子节点初始化
for(int i = 0;i<=m+1;i++){
p->key[i] = -1;
p->ptr[i] = NULL;
p->recptr[i] = NULL;
}
p->keynum = 0;
p->parent = NULL;
return;
}
int InsertBTree(BTree &T, int k, Record rcd);
初始条件:结点 T 存在。
操作结果:在 B 树中插入关键字为 k 的记录 rcd
int InsertBTree(BTree &T, int k, Record rcd){
result result;
//查找是否已经存在
SearchBTree(T, k, result);
if(result.tag) return 0; //已存在则不执行插入
Insert(result.node, k, result.index, NULL, rcd);
BTree ap;
while (result.node->keynum >= MAX_NUM ){
split(result.node, ap);
k=result.node->key[SPLIT_INDEX];
if (result.node->parent == NULL){
//最顶层
newRoot(T, result.node, ap, k, result.node->recptr[SPLIT_INDEX]);
k = -1;
result.node->recptr[SPLIT_INDEX] = NULL;
}
else {
int index = Search(result.node->parent, k); //在双亲结点中查找k的插入位置
Insert(result.node->parent, k, index, ap, result.node->recptr[SPLIT_INDEX]);
}
result.node = result.node->parent; //上移一层
if(result.node==NULL) break;
}
return 1;
}
void split(BTree &q, BTree &ap); //分裂饱和结点
初始条件:结点 q 和结点 ap 已存在。
操作结果:将结点 q 分裂成两个结点,前一半保留,后一半移入结点 ap。
void split(BTree &q, BTree &ap){
//将q结点分裂成两个结点,前一半保留在原节点,另一半移入ap所指向的新节点
int i, j, n = q->keynum;
ap = (BTNode*)malloc(sizeof(BTNode)); //生成新结点
InitBTNode(ap);
ap->ptr[0] = q->ptr[SPLIT_INDEX];
for(i = SPLIT_INDEX + 1, j = 1; i<=n;i++,j++){ //后一半移入ap结点
ap->key[j] = q->key[i];
ap->ptr[j] = q->ptr[i];
ap->recptr[j] = q->recptr[i];
q->key[i] = -1;
q->ptr[i] = NULL;
q->recptr[i] = NULL;
}
ap->keynum = n-SPLIT_INDEX;
ap->parent = q->parent;
for(i=0;i<=n-SPLIT_INDEX;i++) //修改新结点的子节点的parent域
if(ap->ptr[i]!=NULL) ap->ptr[i]->parent = ap;
q->keynum = SPLIT_INDEX - 1; //q的前一半保留,修改keynum
return;
}
void newRoot(BTree &T, BTree p, BTree ap, int k, Record rcd);
初始条件:结点 T,p,ap 已存在。
操作结果:生成新的根结点 T 原 p 和 ap 为子树指针
void newRoot(BTree &t, BTree p, BTree ap, int k, Record rcd){ //生成新的根节点 p是左子树 ap是右子树
t = (BTNode*)malloc(sizeof(BTNode));
t->keynum = 1;
t->ptr[0] = p;
t->ptr[1] = ap;
t->key[1] = k;
t->recptr[1] = rcd;
if(p!=NULL) p->parent = t;
if(ap!=NULL) ap->parent = t;
t->parent = NULL; //新根的双亲是空指针
}
void Insert(BTree &q, int k, int index, BTree ap, Record rcd); //插入关键字及指针ap
初始条件:结点 q 和结点 ap 已存在,0<indexkeynum
操作结果:将关键字 k 和结点 ap 分别插入到 q->key[index+1]和 q->ptr[index+1]中
void Insert(BTree &q, int k, int index, BTree ap, Record rcd){
//关键字k和新结点指针ap分别插入到q->key[i]和q->ptr[i]
int i;
//将待插入结点后的所有结点后移
for(i = q->keynum; i>=index; i--){
q->key[i+1] = q->key[i];
q->ptr[i+1] = q->ptr[i];
q->recptr[i+1] = q->recptr[i];
}
q->key[index] = k;
q->ptr[index] = ap;
q->recptr[index] = rcd;
if(ap!=NULL) ap->parent = q;
q->keynum++;
return;
} //删除索引为key的记录
int DeleteBTree(BTree &T, int key); //删除索引为key的记录
初始条件:B 树 T 已存在
操作结果:在 B 树 T 中删除关键字 key
int DeleteBTree(BTree &T, int key){
//删除索引为key的记录
result r;
SearchBTree(T, key, r);
if (r.tag == 0) return 0;
//若不是终端结点,将直接前驱的索引和值覆盖掉当前结点
if (r.node->ptr[0] != NULL) Successor(r.node, r.index);
//更新key的值为前驱结点
key = r.node->key[r.index];
//从结点p中删除key[i]
Remove(r.node, r.index);
//找到
int index = Search(r.node->parent, key) - 1;
//调整树
if (r.node->parent != NULL && r.node->keynum < MIN_NUM)
Restore(r.node, index);
return 1;
}
void Successor(BTree &node, int &index);
初始条件:B 树 node 已存在
操作结果:将直接前驱的索引和值覆盖掉当前结点
//若不是终端结点,将直接前驱的索引和值覆盖掉当前结点
void Successor(BTree &node, int &index){
if(node == NULL) return;
//寻找直接前驱
if(node->ptr[index - 1] == NULL) return;
BTree p = node->ptr[index - 1];
while(p->ptr[p->keynum] != NULL)
p = p->ptr[p->keynum];
//将直接前驱的索引和值覆盖掉当前结点
node->key[index] = p->key[p->keynum];
node->recptr[index] = p->recptr[p->keynum];
//令node指向待删除的直接前驱结点
node = p;
index = p->keynum;
}
int Remove(BTree& node, int i);
初始条件:结点 node 已存在,0<ikeynum
操作结果:node 结点删除 key[i]和它的孩子指针 ptr[i]
//从结点p中删除key[i]
int Remove(BTree& node, int i){
if (node == NULL) return 0;
//将删除结点后结点前移
for (; i < node->keynum; i++) {
node->key[i] = node->key[i+1];
node->recptr[i] = node->recptr[i+1];
}
node->keynum--;
node->recptr[i] = NULL;
return 1;
}
int InsertRecord(BTree& node, int key,int i, Record rcd);
初始条件:结点 node 已存在,0<ikeynum
操作结果:向父结点插入关键字
//向父结点插入关键字
int InsertRecord(BTree& node, int key,int i, Record rcd) {
if (node == NULL) return 0;
for (int j = node->keynum; j >= i; j--) {
node->key[j + 1] = node->key[j];
node->recptr[j + 1] = node->recptr[j];
}
node->key[i] = key;
node->recptr[i] = rcd;
node->keynum++;
return 1;
}
void Restore(BTree& node, int index);
初始条件:结点 node 已存在,0<indexkeynum
操作结果:调整 node,使得 B 树正常
//调整树
void Restore(BTree& node, int index) {
BTree parent,l_brother, r_brother;
parent = node->parent;
//左兄弟够借
if (index > 0 && (l_brother = parent->ptr[index - 1])->keynum > MIN_NUM) {
//该节点插入直接前驱
Insert(node, parent->key[index], 1, node->ptr[0], parent->recptr[index]);
//改变孩子指针
node->ptr[0] = l_brother->ptr[l_brother->keynum];
if (l_brother->ptr[l_brother->keynum] != NULL) l_brother->ptr[l_brother->keynum]->parent = node;
//移除父节点中其直接前驱
Remove(parent, index);
//将直接前驱的直接前驱插入到父节点
InsertRecord(parent, l_brother->key[l_brother->keynum], index, l_brother->recptr[l_brother->keynum]);
//将直接前驱的直接前驱删除
Remove(l_brother, l_brother->keynum);
}
//左兄弟不够,右兄弟够借
else if (index < parent->keynum && (r_brother = parent->ptr[index + 1])->keynum > MIN_NUM) {
Insert(node, parent->key[index+1], node->keynum + 1, r_brother->ptr[0], parent->recptr[index + 1]);
Remove(parent, index + 1);
InsertRecord(parent, r_brother->key[1], index + 1, r_brother->recptr[1]);
Remove(r_brother, 1);
for (int i=0; i <= r_brother->keynum; i++) r_brother->ptr[i] = r_brother->ptr[i + 1];
}
//兄弟不够借,合并左子树
else if (index > 0) {
l_brother = parent->ptr[index - 1];
Insert(node, parent->key[index], 1, node->ptr[0], parent->recptr[index]);
Remove(parent, index);
CombineBTNode(l_brother, node);
node = l_brother;
for (int i = index; i <= parent->keynum; i++) parent->ptr[i] = parent->ptr[i + 1];
//调整父节点至平衡
if (parent->keynum < MIN_NUM) {
if (parent->parent == NULL) DeleteRoot(parent);
else Restore(parent, Search(parent->parent, node->key[1]) - 1);
}
}
//兄弟不够借,合并右子树
else {
r_brother = parent->ptr[index + 1];
Insert(node, parent->key[index+1],node->keynum + 1, r_brother->ptr[0], parent->recptr[index + 1]);
Remove(parent, index + 1);
CombineBTNode(node, r_brother);
for (int i = index + 1; i <= parent->keynum; i++)
parent->ptr[i] = parent->ptr[i + 1];
//调整父节点至平衡
if (parent->keynum < MIN_NUM) {
if (parent->parent == NULL) DeleteRoot(parent);
else Restore(parent, Search(parent->parent, node->key[node->keynum + 1]) - 1);
}
}
}
void CombineBTNode(BTree& l_node, BTree& r_node);
初始条件:结点 l_node,r_node 已存在
操作结果:将右节点数据调整至左节点,释放右节点
//合并结点
void CombineBTNode(BTree& l_node, BTree& r_node) {
//左结点为空返回
if (l_node == NULL) return;
//将右结点所有记录合并到左结点
for (int i = 1; i <= r_node->keynum; i++) {
Insert(l_node, r_node->key[i], l_node->keynum + 1, r_node->ptr[i], r_node->recptr[i]);
}
//释放右节点
free(r_node);
return;
}
void DeleteRoot(BTree& root);
初始条件:结点 root 已存在
操作结果:将合并后结点中所有记录插入到父节点中
//将合并后结点中所有记录插入到父节点中
void DeleteRoot(BTree& root) {
BTree node = root->ptr[0];
//父节点指向node的孩子
root->ptr[0] = node->ptr[0];
//非终端结点,孩子结点指向父节点
if (node->ptr[0] != NULL) node->ptr[0]->parent = root;
//将所有记录插入到父节点
for (int i = 1; i <= node->keynum; i++) Insert(root, node->key[i], i, node->ptr[i], node->recptr[i]);
//释放该节点
free(node);
return;
}
void Traverse(BTree t, int k);
初始条件:B 树 t 已存在
操作结果:遍历 B 树
//遍历B树
void Traverse(BTree t, int k) {
if (t != NULL) {
int i;
for (i = 1;i <= t->keynum;i++) {
//非终端结点
if (t->ptr[i - 1] != NULL) {
if (i == 1) {
for (int j = 1;j <= (k * 2);j++) printf(" ");
for (int j = 1;j <= t->keynum;j++) {
if (j == t->keynum) printf("%d\n", t->key[j]);
else printf("%d,", t->key[j]);
}
k++;
}
}
//终端结点
else {
if (i == 1 && i == t->keynum) {
for (int j = 1;j <= (k * 2);j++) printf(" ");
printf("%d\n", t->key[i]);
}
if (i == 1 && i < t->keynum) {
for (int j = 1;j <= (k * 2);j++) printf(" ");
printf("%d,", t->key[i]);
}
if (i != 1 && i < t->keynum) {
printf("%d,", t->key[i]);
}
if (i != 1 && i == t->keynum) {
printf("%d\n", t->key[i]);
}
}
if (i == 1)
Traverse(t->ptr[i - 1], k);
Traverse(t->ptr[i], k);
}
}
}
void PrintBTree(BTree t);
初始条件:B 树 t 已存在
操作结果:遍历打印 B 树
//凹入表输出
void PrintBTree(BTree t) {
printf("当前B树的状态如下:\n");
if (t == NULL)
printf("此B树为空树\n");
else
Traverse(t, 0);
}
int InsertBTree(BTree &T, int k, Record rcd);
void InitBTNode(BTree &p){
p = (BTree)malloc(sizeof(BTNode));
//对p的子节点初始化
for(int i = 0;i<=m+1;i++){
p->key[i] = -1;
p->ptr[i] = NULL;
p->recptr[i] = NULL;
}
p->keynum = 0;
p->parent = NULL;
return;
}
void SearchBTree(BTree T, int k, result &r);
初始条件:结点 T 已存在
操作结果:在结点 T 中查找关键字 k 的插入位置 i,1) 用 r 返回(pt,i,tag)
void SearchBTree(BTree t, int k, result &r){
int i = 0, found = 0;
//在m阶B树t上查找索引k,用r返回(pt,i,tag)
//如果查找成功,则标记tag=1,指针pt所指结点中第i个索引等于k
//否则tag=0,若要插入索引为k的记录,应位于pt结点中第i-1个和第i个索引之间
BTree p = t, q = NULL;
while(p!=NULL&&0==found){
i = Search(p, k); //在p->key[1..p->keynum]中查找p->key[i-1]<k<=p->key[i]
if(i<=p->keynum && p->key[i]==k) found = 1; //找到待查索引
else{
q = p;
p = p->ptr[i-1];
}//指针下移
}
if(1 == found){//查找成功,返回k的位置p及i
r.node = p; r.index = i; r.tag = 1;
}else{ //查找不成功,返回k的插入位置q及i
r.node = q; r.index = i; r.tag = 0;
}
return;
}
int Search(BTree p, int k);
初始条件:结点 p 已存在
操作结果:在 p->key[1…p->keynum]中查找 p->key[i-1]<k<=p->key[i]
//在p->key[1..p->keynum]中查找p->key[i-1]<k<=p->key[i]
int Search(BTree p, int k){ //在p->key[1..p->keynum]找k
int i=1;
while(i <= p->keynum&&k>p->key[i]) i++;
return i;
}
六、时间复杂度及优缺点
存储结构 | B 树 | |
---|---|---|
InitBTNode(BTree &p); | O(n) | |
InsertBTree(BTree &T, int k, Record rcd); | O(n) | |
split(BTree &q, BTree &ap); | O(n) | |
newRoot(BTree &T, BTree p, BTree ap, int k, Record rcd); | O(1) | |
Insert(BTree &q, int k, int index, BTree ap, Record rcd); | O(n) | |
DeleteBTree(BTree &T, int key); | O(nlogn) | |
Successor(BTree &node, int &index); | O(n) | |
Remove(BTree& node, int i); | O(n) | |
InsertRecord(BTree& node, int key,int i, Record rcd); | O(1ogn) | |
Restore(BTree& node, int index); | O(1ogn) | |
基 本 操 作 时 间 复 杂 度 |
CombineBTNode(BTree& l_node, BTree& r_node); | O(logn) |
DeleteRoot(BTree& root); | O(logn) | |
Traverse(BTree t, int k); | O(logn) | |
PrintBTree(BTree t); | O(logn) | |
SearchBTree(BTree T, int k, result &r); | O(nlogn) | |
Search(BTree p, int k); | O(n) | |
void DestroyBTree(BTree &t) | O(logn) | |
优 缺 点 分 析 |
优点 | 在文件系统中有所应用,适合作为文件的索引;当查找的值恰好处在一个非叶子节点时,查找到该节点就会成功并结束查询 |
缺点 | 不适合使用磁盘读取 |
七、功能测试
7.1 插入功能
测试用例:
0 1 2 3 4 5 6
24 10 30 4 25 44 59
输出 B 树状态如下
7.2 删除功能
测试用例:
0 1 2 3 4 5 6
24 10 30 4 25 44 59
删除索引为 2 和 6 的数据,输出 B 树的状态
7.3 搜索功能
测试用例:
0 1 2 3 4 5 6
24 10 30 4 25 44 59
搜索索引为 1 和 6 的结点
7.4 释放 B 树
测试用例:
0 1 2 3 4 5 6
24 10 30 4 25 44 59
释放整颗 B 树
八、思考与小结
-
在部分需要判空的地方没有判空
-
本次基本实现了增删改查的全部功能,且实现可拓展 m 阶 B 树
-
课本以及数据结构实验手册对于 B 树的介绍和代码过于缺少,因此本次抽象数据类型的实现最难的地方在于 B 树的学习与理解,利用哔哩哔哩教学与编程网站理解最后实现 B 树的接口,融会贯通,从测试结果来看,本次实验实现结果满意,可见对于 B 树的掌握较好,同时在编程细节上也有较好的体现,对于边界值,异常值,指针判空等都有注意到位。
用磁盘读取 |
七、功能测试
7.1 插入功能
测试用例:
0 1 2 3 4 5 6
24 10 30 4 25 44 59
输出 B 树状态如下
7.2 删除功能
测试用例:
0 1 2 3 4 5 6
24 10 30 4 25 44 59
删除索引为 2 和 6 的数据,输出 B 树的状态
7.3 搜索功能
测试用例:
0 1 2 3 4 5 6
24 10 30 4 25 44 59
搜索索引为 1 和 6 的结点
7.4 释放 B 树
测试用例:
0 1 2 3 4 5 6
24 10 30 4 25 44 59
释放整颗 B 树
八、思考与小结
-
在部分需要判空的地方没有判空
-
本次基本实现了增删改查的全部功能,且实现可拓展 m 阶 B 树
-
课本以及数据结构实验手册对于 B 树的介绍和代码过于缺少,因此本次抽象数据类型的实现最难的地方在于 B 树的学习与理解,利用哔哩哔哩教学与编程网站理解最后实现 B 树的接口,融会贯通,从测试结果来看,本次实验实现结果满意,可见对于 B 树的掌握较好,同时在编程细节上也有较好的体现,对于边界值,异常值,指针判空等都有注意到位。