树 - 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树的简单实现的主要内容,如果未能解决你的问题,请参考以下文章

每日算法二叉树的遍历

剑指offer-树的子结构

树的子结构

10.STL简单红黑树的实现

C++AVL树的简单实现及验证

C++AVL树的简单实现及验证