1. 适用范围:
- 减少对硬盘中不同数据块的切换次数
- 加快访问硬盘中的数据
- 与主存的访问时间相比,磁盘的 I/O 操作相当耗时
- 而B-树就可以减少数据块之间的切换, 减少磁盘I/O 操作次数
- 存储大量数据, 又要保证查找性能
2. 梗概:
B
/B-
树是一种平衡的多路查找树:
B-树有两种定义: 最小度定义 和 m阶定义
最小度定义:
- 所有的叶子结点都在同一层
- 每个结点包含的 子树总数有上界和下界
- 即 非根结点的子树总数∈[t,2t]
- 用一个被称为 B-树的 最小度数 的固定整数 t 来表示这些界 ,其中 t 取决于磁盘块的大小
- 因此非根结点包含的 关键字数量∈[t-1,2t-1]
- 根结点 子树总数∈[2,2t]
- 即 非根结点的子树总数∈[t,2t]
- 每个结点的子树要么全为空, 要么全非空
3. 性质:
- 关键字与左右子树的位置关系
- keys[i]的左子树为Children[i]
- keys[i]的右子树为Children[i+1]
- Children[i+1]=keys[i]的右子树=keys[i+1]的左子树
- B-树插入结点是从叶子往根生长变高的
4. 操作:
- 遍历
- 查找
- 分裂
- 插入
- 插入前先查找到要插入的位置
- 查找插入往往需要伴随着分裂
- 分裂与插入顺序有两种关系:
- 查找的时候就分裂沿途已满的结点
- 插入后再往上逐渐分裂已满的结点
- 分裂的具体操作:
- 需要知道该已满结点在双亲结点中的子树位置i
- 把已满结点的中位关键字单独分离出来
- 但其左右子树并不分离
- 从中位把已满结点分为长度相等的两半
- 左边那一半包括第一个子树~中位关键字的左子树
- 右边那一半包括最后一个子树~中位关键字的右子树
- 双亲结点将多插入一个关键字和一个子树
- 中位关键字插在第i位
- 这时自然地, 中位关键字的左子树为左半分裂结点
- 右半分裂结点插入在第i+1位
- 作为中位关键字的右子树
- 中位关键字插在第i位
- 分裂与插入顺序有两种关系:
- 删除
5. 在c语言中的实现
1. 定义:
代码:
#define t 3
typedef struct BTreeNode{
/*如果已满,中位数为keys[t-1]*/
int keys[2*t-1];
/*keys[i]的左孩子为Children[i],右孩子为i+1*/
struct BTreeNode* Children[2*t];
int keynum;
char leaf;//用来标记是否叶子节点
}BTreeNode,*BTree;
2. 遍历:
void traverse(BTree root){
if(!root)return;
int i;
/*递归通过关键字逐个递归遍历所有子树*/
for(i=0;i<root->keynum;++i){
if(!root->leaf)
traverse(root->Children[i]);
/*操作root->keys[i]*/
}
/*遍历递归最右子树*/
if (!root->leaf)
traverse(root->Children[i]);
}
3. 查找:
递归方式:
BTreeNode *search(BTree root,int key){
if(!root)return NULL;
int i=0;
/*在当前结点中找到大于等于关键字的*/
for(;i<root->keynum && root->keys[i]<key;++i);
/*递归出口*/
if(root->keys[i]==key)
return root;
else if(root->leaf)return NULL;
/*对应子树下递归搜索*/
else return serach(root->Children[i],key);
}
4. 插入操作:
4.1. 前置插入的方式:
梗概:
- 采用递归的方式
- 方便回溯沿途结点
- 方便找到沿途已满的结点, 然后分裂它
- 方便回溯沿途结点
- 在递归前先初始化
- 初始化空树
- 初始化已满的根结点
- 递归查找并插入
- 递归出口: 当前结点为叶结点
- 把关键字插入到当前叶结点
- 递归的同时分裂沿途上已满的结点
- 分裂操作:
- 创建一个新的结点保存右半分裂部分
- 把中位关键字和右半分裂部分别插入双亲结点中
- 中位关键字对应左半分裂部分的位置(i)
- 右半分裂部分对应中位关键字的右子树位置(i+1) 代码:
- 分裂操作:
- 递归出口: 当前结点为叶结点
void insertNonFull(BTree root,int key);
void splitChild(BTreeNode *parent,int i,BTreeNode *son);
void insert(BTree* root,int key){
/*为递归插入初始化树*/
if(*root ==NULL){
BTreeNode* s=(BTreeNode*)calloc(1,sizeof(BTreeNode));
s->keys[0]=key;s->keynum=1;s->leaf=0;
*root = s;
}
/*为递归插入初始化分裂根结点*/
else if((*root)->keynum>=2*t-1){
/*往上生长,构建新根*/
BTreeNode *s=(BTreeNode*)calloc(1,sizeof(BTreeNode));
s->Children[0]=*root;
/*把新根作为整个树的根*/
*root=s;
splitChild(s,0,*root);
/*分裂后有两个子树,查找需要往那颗子树递归插入*/
int i=0;
for(;s->keys[0]<key;++i)
insertNonFull(s->Children[i],key);
}
else insertNonFull(*root,key);
}
void insertNonFull(BTree root,int key){
int i=root->keynum-1;//从后往前遍历
if(root->leaf){//递归出口
/*采用直接插入排序法*/
for(;i>=0 && root->keys[i]>key;--i)
root->keys[i+1]=root->keys[i];
root->keys[i+1]=key; root->keynum++;
return;
}
/*找到已有键值中最接近的,然后根据下标找到对应的子树*/
for(;i>=0 && root->keys[i]>key;)--i;
/*查找插入位置的同时分裂沿途已满的结点*/
if(root->Children[i+1]->keynum==2*t-1){
splitChild(root,i+1,root->Children[i+1]);
/*下层分裂后有关键字新增到i+1处,判断其是否是递归插入的目标*/
if(root->keys[i+1])i++;
}
/*递归找到叶结点并插入*/
insertNonFull(root->Children[i+1],key);
}
void splitChild(BTreeNode *parent,int i,BTreeNode *son){
//把parent结点下的第i颗子树son分裂
BTreeNode *z=(BTreeNode*)calloc(1,sizeof(BTreeNode));
z->keynum=t-1;//对半分裂后剩t个,抽去最前面一个键值,减1个
z->leaf=son->leaf;
/*把原结点son的中位数以后的树都赋给新结点z*/
for(int j=0;j<t-1;j++)
z->keys[j]=son->keys[j+t];
son->keynum=t-1;//更新分裂后的关键字数量
/*把原结点son的后半子树都赋给新节点z*/
if(!(son->leaf))
for(int j=0;j<t;++j)
z->Children[j]=son->Children[j+t];
/*因为要有新关键字插入parent,所以要在i处腾空位*/
for(int j=parent->keynum-1;j>=i;--j)
parent->keys[j+1]=parent->keys[j];
parent->keys[i]=son->keys[t-1];parent->keynum++;
/*因为有新的子树插入parent,所以要在i+1处腾空位*/
for(int j=parent->keynum+1;j>=i+1;--j)
parent->Children[j+1]=parent->Children[j];
parent->Children[i+1]=z;
}