树 - B树的简单实现

来源:互联网 发布:手机千牛淘宝客插件 编辑:程序博客网 时间:2024/06/05 10:03

二叉查找树和平衡二叉树都是典型的二叉查找树结构,其查找的时间复杂度与树的高度相关,降低树的高度自然对查找效率有所帮助,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 0const 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树中插入关键字keyint 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开发艺术探索》,简单的就直接划书,难的又看不懂,唉,博客总结也落下了

最后如果本文有哪个地方不足或者错误,还请指正!

1 0
原创粉丝点击