树 - B树的简单实现
Posted Y_ZhiWen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树 - B树的简单实现相关的知识,希望对你有一定的参考价值。
二叉查找树和平衡二叉树都是典型的二叉查找树结构,其查找的时间复杂度与树的高度相关,降低树的高度自然对查找效率有所帮助,B树正是这样的树。
定义
一棵m阶B树,或为空树,满足以下特性:
- 树中每个结点最多有m棵子树
- 若根结点不是叶子节点,则至少有2个子树
- 除根结点之外的所有非终端结点至少有 ⌈m/2⌉ 棵子树
- 每个非终端结点中包含信息(n , A0 , K1 , A1 , K2 , .. , Kn , An)。其中
- Ki(1 ≤ i ≤ n)为关键字,且按升序排序
- 指针Ai(0 ≤ i ≤ n)指向子树的根结点,Ai-1 指向子树中所有结点的关键字均小于Ki,且大于Ki-1
- 关键字的个数n必须满足 ⌈m/2⌉ -1 ≤ n ≤ m-1
- 所有叶子结点都出现在同一层,叶子结点不包含任何信息
这里我发现跟定义跟算法导论一书中似乎不一样,有待研究呀!
结构
#define M 7 //阶数
#define SUCCESS 1
#define ERROR 0
const int MIN = ceil(M/2.0)-1;
const int MAX = M-1;
typedef struct BTNode
int size; // 当前关键字大小
int keys[M+1]; // 关键字数组,0号单元未使用,按升序排序
struct BTNode *parent; // 双亲结点指针
struct BTNode *childs[M+1]; // 孩子结点数组
BTNode,*BTree;
typedef struct
BTree btree;// 指向找到的结点
int i; // 1<= i <= M,在结点中关键字数组的位置
int tag; // 标记查找是否成功,1为成功,2为失败
Result;
接口
基本的接口有查找、插入、删除,下面分别讲解
查找
由B树的定义,孩子结点指针Ai中的关键字均小于Ki,大于Ki-1。先从根结点开始查找,若找到要查找的关键字,则查找成功;否则则根据比较进入孩子结点Ai查找,如此反复,直到找到或者查找到叶子结点为止。
下面给出相关代码:
// 比较关键字a与b的大小
// 1: a>b
// 0: a==b
// -1 a<b
int CompareTo(int a, int b)
return a>b? 1 : a == b ? 0 : -1;
// 在结点t中查找关键字key
// 返回 的index表示keys[index]之前的关键字都小于key
int SearchKey(BTree &t,int key)
int index = 1; // 从1开始
// 保证index小于关键字数组的大小,同时保证keys[index]之前的关键字都小于key
while( index <= t->size && CompareTo(key,t->keys[index]) == 1 )
index++;
return index;
// 查找关键字key
// 返回结果res 查找成功则返回位置,否则则返回待插入位置
void Search(BTree &t,int key,Result &res)
BTree p,q; // p表示当前节点,q表示上一个结点
p = t;
q = NULL;
int isFound = 0; // 表示是否查找成功,0为不成功,1为成功
int index = 0;
while(p!=NULL&&isFound==0)
// 在当前节点查找
index = SearchKey(p,key);
if( index <= p->size && CompareTo(key, p->keys[index]) == 0 ) isFound = 1;
else
q = p;
p = p->childs[index-1];
if(isFound == 1)
res.btree = p;
res.tag = isFound;
res.i = index;
else
// 查找不成功,返回key的插入位置
res.btree = q; // 待插入结点
res.i = index; // 待插入的位置
res.tag = isFound;
插入
利用前述的查找结果,若找到,则直接返回,否则,根据待插入位置插入。插入后,若其关键字总数size为达到m,结束,否则分裂结点
分裂结点:
- 生成一个新结点,从中间位置把结点(不包括中间位置结点)分成两部分。
- 前半部分留在旧结点中,后半部分复制到新节点中,中间位置连同新结点插入到父结点中。
- 如果父结点的关键字个数也超过m-1,再分裂。如果持续到根结点分裂,则会产生新的根结点
下面是代码,有点多,不过逻辑还是挺清晰的,而且还有注释:
// 创建以t为根的根结点
void NewRoot(BTree &t,BTree p,int key,BTree ap)
t = (BTNode*)malloc(sizeof(BTNode));
t->keys[1] = key;
t->childs[0] = p;
t->childs[1] = ap;
t->size = 1;
t->parent = NULL;
if(p!=NULL) p->parent = t;
if(ap!=NULL) ap->parent = t;
// 将关键字插入结点t的index位置,ap为其子结点
void InsertToNode(BTree &t,int key,int index,BTree &ap)
int n = t->size;
for(int i = n; i >= index; i--)
t->keys[i+1] = t->keys[i];
t->childs[i+1] = t->childs[i];
t->keys[index] = key;
t->childs[index] = ap;
if( ap!=NULL ) ap->parent = t;
t->size++;
// 将结点t分裂成两个结点,前一半保留,后一半移入新结点ap
void Split(BTree &t,int s,BTree &ap)
int n = t->size;
ap = (BTNode*)malloc(sizeof(BTNode));
int i, j;
for( i = s+1, j = 1; i <= n; i ++, j++ )
ap->keys[j] = t->keys[i];
for( i = s, j = 0; i <= n; i++, j++)
ap->childs[j] = t->childs[i];
if(ap->childs[j]!=NULL) ap->childs[j]->parent = ap;
ap->size = n - s;
ap->parent = t->parent;
t->size = s - 1;
// 在B树中插入关键字key
// q:B树上结点,又查找失败得到,待插入位置为q->keys[index-1]和q->keys[index]之间
void InsertToTree(BTree &t,int key,BTree q,int index)
int mFinished = 0; // 标记是否插入完成
int isNeedNewRoot = 0; // 标记是否需要创建新的根结点
int mKey = key; // 关键字
if( q == NULL )
NewRoot(t,NULL,mKey,NULL);
else
BTree ap = NULL;
while(isNeedNewRoot == 0 && mFinished == 0)
InsertToNode(q,mKey,index,ap);
if(q->size < M)
mFinished = 1;
else
// 需要分裂结点q
int s = (M+1)/2;
mKey = q->keys[s];
Split(q,s,ap);
if(q->parent != NULL)
// 将结点被分裂出来的关键字mKey插入父结点中
q = q->parent;
index = SearchKey(q,mKey);
else
// 此时q原本是根结点,被分裂成结点q和ap
isNeedNewRoot = 1;
if(isNeedNewRoot == 1)
NewRoot(t,q,mKey,ap);
// 在B树中插入关键字key
int Insert(BTree &bt,int key)
Result result;
Search(bt,key,result);
if( result.tag == ERROR )
InsertToTree(bt,key,result.btree,result.i);
return SUCCESS;
return ERROR;
删除
B树的删除操作就有点复杂,要分多种情况考虑,最重要的还是对定义要有深刻的理解,特别是在删除过程,必须明确现在的B树是满足定义的,因为在插入跟删除都不断维持着B树的状态,所以不要做无谓的判断,这一点我自己确实做得不好
思路:
先通过查找操作查找待删除关键字位置,再根据所在结点是否最下层非终端结点处理
- 若该结点为最下层非终端结点,有三种可能情况:
- 待删除的关键字所在结点关键字个数 n ≥ ⌈m/2⌉ ,直接删除
- 待删除的关键字所在结点关键字个数 n == ⌈m/2⌉ - 1,删除后需要调整(下面说明)
- 若该结点不是最下层非终端结点,且被删关键字为该结点中第 i 个关键字,则可从孩子结点child[i]所指子树中找到最下层非终端结点的最小关键字key,代替key[i],然后删除key,这样就回到删除最下层非终端结点的操作
调整过程:
当最下层非终端结点被删除某关键字后,需要进行调整
- 如果其左右兄弟结点中有“富余”的关键字,则可将右(左)兄弟结点中最小(大)关键字上移至双亲结点。而将双亲结点中的小(大)与该上移关键字下移至被删关键字所在结点中。(注意这里左右兄弟结点指的是间隔为1的兄弟结点)
- 如果其左右兄弟结点中没有“富余”的关键字,需要把要删除关键字结点与其左(或右)兄弟结点以及双亲结点中分割二者的关键字合并成一个结点,即在删除关键字后,加上双亲结点间隔兄弟结点的关键字,合并到兄弟结点中去。如果因此双亲结点中关键字个数小于 ⌈m/2⌉ - 1,则对双亲结点做同样处理。甚至可能对根结点做这样处理使得树减少一层。
下面是代码,代码有点长,最重要还是思路:
// 移除结点t中pos位置的关键字 和child[pos-1]
void RemoveKey(BTree &t,int pos)
int size = t->size;
int i;
for(i = pos; i < size; i ++)
t->keys[i] = t->keys[i+1];
// 孩子结点为空,可以不用管 ,
// 不能不管 , 可能在Restore2中调用非终端结点的
// t->childs[i] = t->childs[i+1]; // 但是这样不行,另外下面方法调用
t->childs[i-1] = t->childs[i]; // pos >= 1
t->childs[i-1] = t->childs[i];
t->size --;
// 查找父结点parent中子结点t的位置,查找不到则返回-1
int SearchChildPos(BTree parent,BTree t)
if(t == NULL)
return -1;
int size = parent->size;
for(int i=0; i <= size; i ++)
if(parent->childs[i] == t)
return i;
return -1;
// 删除树上结点t的pos个位置
void DeleteBTree(BTree &t,int pos)
if( t==NULL ) return ;
if(t->childs[pos] != NULL) // 不是最下层终端结点
// 查找孩子子树最下层非终端结点最小关键字
BTree child = t->childs[pos];
while( child->childs[0] != NULL)
child = child->childs[0];
int key = child->keys[1];
t->keys[pos] = key;
// 调用delete
DeleteBTree(child,1);
else // 下层终端结点
RemoveKey(t,pos);
if( t->size < MIN )
Restore(t,pos);
// 删除B树t上的关键字key
int Remove(BTree &t,int key)
Result res;
Search(t,key,res);
if(res.tag == 1)
DeleteBTree(res.btree,res.i);
return 1;
else
printf("无该关键字\\n");
return 0;
// 查看兄弟结点是否富余
// 若富余,调整后返回 SUCCESS 1
// 否则 返回 ERROR 0
int Restore2(BTree &t)
if( t == NULL ) return ERROR;
if( t->size >= MIN ) return SUCCESS;
BTree parent = t->parent;
if( parent == NULL ) return ERROR;
int sizeOfParent = parent->size;
int posInParent;
if((posInParent = SearchChildPos(parent,t)) == -1 ) return ERROR;
if( posInParent > 0 && parent->childs[posInParent-1] != NULL && parent->childs[posInParent-1]->size > MIN ) // 查找左兄弟,仅仅左兄弟
// 如果有,则获取左兄弟最大关键字
BTree lbt = parent->childs[posInParent-1];
int leftMaxKey = lbt->keys[lbt->size];
// 注意这里移动后,结点的父结点的处理
InsertToNode(t,parent->keys[posInParent],1,t->childs[0]);
t->childs[0] = lbt->childs[lbt->size];
if(t->childs[0]) t->childs[0]->parent = t;
lbt->childs[lbt->size] = NULL;
lbt->size--;
parent->keys[posInParent] = leftMaxKey;
return SUCCESS;
else if( posInParent < sizeOfParent && parent->childs[posInParent+1] != NULL && parent->childs[posInParent+1]->size > MIN )// 查找右兄弟
// 如果有,则获取右兄弟最小关键字
BTree rbt = parent->childs[posInParent+1];
int rightMinKey = rbt->keys[1];
// 注意这里移动后,结点的父结点的处理
InsertToNode(t,parent->keys[posInParent+1],t->size+1,rbt->childs[0]);
RemoveKey(rbt,1);
// 插入父结点
parent->keys[posInParent+1] = rightMinKey;
return SUCCESS;
return ERROR;
// 与兄弟结点合并操作,操作后递归对父结点进行调整
void Restore3(BTree &t)
if( t == NULL ) return ;
if(t->size>=MIN)
return;
BTree parent = t->parent;
if( parent == NULL )
// ????为什么这样不行
// t = t->childs[0];
if( t->size > 0 )
return ;
BTree tt = t->childs[0];
if( tt == NULL ) return ;
if(t->childs[0]) t->childs[0]->parent = NULL;
t->childs[0] = NULL;
int size = tt->size;
for(int i = 1; i <= size; i ++)
t->keys[i] = tt->keys[i];
for(int i = 0; i <= size; i++)
t->childs[i] = tt->childs[i];
if( t->childs[i] ) t->childs[i]->parent = t; // T^T ...
t->size = size;
t->parent = NULL;
return ;
int posInParent;
if((posInParent = SearchChildPos(parent,t)) == -1)
return ;
int sizeOfParent = parent->size;
int mIsFinished = 0;
// 与父结点和左结点结合
if( posInParent > 0 && mIsFinished == 0)
// .......................................可能刚刚好该结点为空 ,不用考虑T^T,还是去看看B树的性质吧。。
BTree lbt = parent->childs[posInParent-1];
if(lbt == NULL)
lbt = (BTNode*)malloc(sizeof(BTNode));
if( lbt == NULL ) return;
lbt->parent = parent;
parent->childs[posInParent-1] = lbt;
lbt->size = 0;
if(lbt != NULL )
lbt->keys[++lbt->size] = parent->keys[posInParent];
lbt->childs[lbt->size] = NULL; // ..........................因为lbt结点结合t结点,而t结点为终端结点,自然没有孩子
int i,j;
for(j = lbt->size+1, i = 1; i <= t->size;i++, j++)
lbt->keys[j] = t->keys[i];
// lbt->childs[j] = NULL;
for( int k = lbt->size, i = 0; i <= t->size; i ++, k++)
lbt->childs[k] = t->childs[i];
if(t->childs[i]) t->childs[i]->parent = NULL;
t->childs[i] = NULL;
if(lbt->childs[k]!=NULL) lbt->childs[k]->parent = lbt; // 居然还漏了 。。。
lbt->size+=t->size;
t->size = 0;
// 移除父结点中所结合的结点 、移除孩子
if(parent->childs[posInParent]) parent->childs[posInParent]->parent = NULL;
parent->childs[posInParent] = NULL;
for(int i = posInParent; i < parent->size; i ++)
parent->keys[i] = parent->keys[i+1];
parent->childs[i] = parent->childs[i+1];
parent->size --;
mIsFinished = 1;
// 与父结点和右结点结合
if( posInParent < sizeOfParent && mIsFinished == 0 )
BTree rbt = parent->childs[posInParent+1];
// 应该移入有结点中,为什么?? 因为t为终端结点
t->keys[++t->size] = parent->keys[posInParent+1];
t->childs[t->size] = NULL;
int i, j;
if(rbt!=NULL)
for(i = 1, j = t->size+1; i <= rbt->size; i ++,j ++)
t->keys[j] = rbt->keys[i];
for( int k = t->size, i = 0; i <= rbt->size; i ++, k ++)
t->childs[k] = rbt->childs[i];
if(rbt->childs[i]) rbt->childs[i]->parent = NULL;
rbt->childs[i] = NULL;
if(t->childs[k]!=NULL) t->childs[k]->parent = t; // 居然还漏了 。。。
t->size += rbt->size;
rbt->size = 0;
// 移除父结点中所结合的结点 、移除孩子
// RemoveKey(parent,posInParent);
if(parent->childs[posInParent+1]) parent->childs[posInParent+1]->parent = NULL;
parent->childs[posInParent+1] = NULL;
for(i = posInParent+1; i < parent->size; i ++)
parent->keys[i] = parent->keys[i+1];
parent->childs[i] = parent->childs[i+1];
parent->size --;
mIsFinished = 1;
// 不能直接调用下面方法删除,因为该结点不是终端结点时,会获取最小结点替代
// int kk = parent->keys[posInParent+1];
// DeleteBTree(parent,posInParent+1);
// 调整父结点
Restore2(parent);
Restore3(parent);
// 调整
void Restore(BTree &t,int pos)
if( Restore2(t) == ERROR )
Restore3(t);
总结
这个B树的抽象数据类型是之前写的,都是泪啊,定义不熟悉,而且代码不严谨,在移动或者调整的时候,孩子结点经常忘了其父结点等等错误,这就有了我当时两三天的调试T^T,醉了。
理解好定义,写代码之前一定思考
这段时间考试,各种预习,关于编程,所以只看了下《android开发艺术探索》,简单的就直接划书,难的又看不懂,唉,博客总结也落下了
最后如果本文有哪个地方不足或者错误,还请指正!
以上是关于树 - B树的简单实现的主要内容,如果未能解决你的问题,请参考以下文章