B-Tree B-树 代码介绍-20230505(C语言)

news/2024/7/5 3:19:20

B-Tree B-树 代码介绍-20230505(C语言)

  1. 前言

前面已经介绍B-树属于多路查找树,它在数据库和文件储存系统设计中有着广泛的应用,B-Tree的基本操作包含查询、插入和删除等基本操作,由于这些操作过程中,B-树必须恪守其基本特性,尤其是结点中关键字数量的限制,导致在插入过程中产生“分裂”操作或在删除过程中需要“合并”结点。

本文着重探讨代码实现过程和思路,对于B-树的相关基本知识,大家可以参考相关书籍或上一篇文章关于B-树的介绍。

  1. B-树操作实现

2.1 数据结构定义

如之前介绍,B-树的关键字插入或删除,可以采用自上而下或者自下而上中的任何策略。本文中描述的算法思路来自严蔚敏《数据结构》,采用的自下而上的程序结构,如有兴趣,也可参考《算法导论》中的自上而下的操作思路。

本文中涉及的无论删除还是插入操作,切入点都是B-树的最底层非叶子结点,由于要不断追寻上一层结点,所以数据结构中自然而然就需有双亲结点进行跟踪,根据树的定义,需要定义关键字数组和关键字信息指针,同时还需包括指向子树的指针。

数据结构定义:

#define m 3 //m 代表B-树的阶数


typedef int   KeyType;
typedef char* Record;


typedef struct  SElemType
{
    KeyType key;
    Record  value;
}SElemType;

typedef struct SSTable
{
    SElemType *elem;
    int        len;
} SSTable;


typedef struct BTNode
{
    int keynum; //number of keys in each node

    struct BTNode *parent; //parent node pointer

    KeyType   key[m+1]; //record in the node
    Record    recptr[m+1];//record pointer in the node

    struct BTNode *ptr[m+1]; //substree pointer

}BTNode, *BTree;

B-tree的搜索不断搜索关键字以及寻找指向子树指针的过程,这两个过程不断交替,直至到叶子结点结束位置,如果找到合适的关键字,那么就需要返回至少两个关键信息: 结点的指针和关键字在结点中所处位置,同时考虑查询失败的情况,需要一个标志位来指示查找是否成功。基于上述需求,我们定义搜索结果的返回对象的数据结构包含上述需求。

typedef struct Result
{
    BTNode *pt; //pointing to the target node
    int i;  // the index in each node[1....m];
    int tag; //1=found; 0=not found
}Result;

2.2 B-树查找操作实现

B-树查找需要在寻找结点与在结点中寻找交叉进行,它涉及到树的结点的查找,以及对结点中有序关键字的查找,对结点中的有序关键字,可以采用二叉查找树算法进行搜索;对于结点(指针)查找,可以选择迭代或递归方式进行。

  • 迭代方式查找
#define EQ(a,b) ((a)==(b))
#define LT(a,b) ((a)<(b))
#define LQ(a,b) ((a)<=(b))


Result search_btree(BTree bt, KeyType key)
{
    int i;
    Result res;
    bool found=false;
    BTree p;
    BTree q;

    i=0;
    p=bt;
    q=NULL; //q is the parent of p

    while(!found && p)
    {
        i=search_index(p,key);

        if(EQ(key,p->key[i]))
        {
            found=true; //find the key successfully
        }
        else
        {
            q=p;
            p=p->ptr[i];
        }
    }

    if(found)
    {
        res.i=i;
        res.pt=p;
        res.tag=1;
    }
    else
    {
        res.i=i;
        res.pt=q;
        res.tag=0;
    }

    return res;
}


int search_index(BTree p, KeyType key)
{
    int low;
    int high;
    int mid;

    low=1;
    high=p->keynum;

    while(low<=high)
    {
        mid=(low+high)/2;

        if(EQ(key,p->key[mid])) //if found, returning the mid position
        {
            return mid;
        }
        else if(LT(key,p->key[mid]))
        {
            high=mid-1;
        }
        else
        {
            low=mid+1;
        }
    }

    // if not found, it will return high(high <low at this point)
    return high; 
    
}

采用递归方式,需要保留前面查询的结果,对于search_index函数可以与上面的迭代查找共用,函数体中需要保留前面查询的结点指针值和指针所在位置。

// p will start with NULL it will act as parent node of q node
Result search_btree_recursion(BTree q, BTree p, int index, KeyType key)
{
    
    if(q!=NULL)
    {
        index=search_index(q,key);
        if(index>0 && q->key[index]==key) //Recursion termination condition -2
        {
            Result res;

            res.i=index;
            res.pt=q;
            res.tag=1;

            return res;
        }
        else
        {
           return search_btree_recursion(q->ptr[index],q,index,key);
        }
    }
    else //Recursion termination condition-1
    {
        Result res;

        res.i = index;
        res.pt = p;
        res.tag = 0;


        return res;
    }
}
  1. 3 B-树插入操作实现

插入操作过程比较复杂,需要先找到待插入结点所在结点指针,这个结点一定位于最底层非叶子节点上,和二叉树插入类似,结点的插入操作首先发生在最底层非叶子节点上,然后根据插入后的结点中关键字数量,决定是否产生“分裂”操作还是插入就此完成。

当插入结点中的关键字数量达到一定阈值后,它就会触发分裂操作,如果其双亲结点也同样达到阈值,那么就一直向上分裂直至满足要求位置,如果根节点满足分裂条件,那么B-tree 的深度就增加1,前面提到过,B-树深度增加(成长)的唯一途径就是根节点分裂。

分裂结点的实现算法具体来说,把一个结点(m,A[0],(K[1],A[1]),…,(K[m],A[m]))分裂为两个结点,此时可将这个满结点分裂为:

p=(m/2-1,A[0],(K[1],A[1]),…,(K[m/2-1],A[m/2-1]))

p’=(m-m/2,A[m/2],(K[m/2+1],A[m/2+1]),…,(K[m],A[m]))

而关键字K[m/2]和p’一起插入到p的双亲结点中去。具体代码实现中,采用memcpy原 结点中q中的右半部分数据赋值给新建结点,然后对此结点的各个特性进行初始化,新结点的父节点和p结点的父节点一致,含有关键字的数量为m-s。由于原结点分裂,原来结点关键字数量需要更新,关键字数量从m减少至m/2-1。

考虑到新建结点中的子树指针的父节点仍然为p,需要对父节点的连接进行更新,更新后的父节点指针为p’,这里当然需要判断前提条件,如果分裂的结点子树为叶子结点,叶子结点不包含任何有用的信息,其子树指针为空,自然而然就无需更新父节点的指针。

void split_node(BTree q, int s, BTree *ap)
{
    int i;
    BTree new_node;

    new_node=(BTree)malloc(sizeof(BTNode));

    memcpy(new_node->ptr,q->ptr+s,sizeof(BTNode *)*(m-s+1));
    memcpy(new_node->key+1, q->key + s + 1, sizeof(KeyType)*(m-s));
    memcpy(new_node->recptr+1, q->recptr + s + 1, sizeof(Record) * (m - s));

    new_node->keynum=m-s;
    q->keynum=s-1;
    *ap=new_node;
    (*ap)->parent=q->parent;

    //Build the parent link if the (*ap)->ptr[i] is not NULL
    for (i = 0; i <= (*ap)->keynum; i++)
    {
        if ((*ap)->ptr[i])
            (*ap)->ptr[i]->parent = *ap;
    }

    return;
}

如果需要在已有的节点上插入Key[i]和A[i],需要对插入点之后的元素向右移动,然后空出指定的插入位置。最后进行赋值操作完成插入操作即可。函数中需要包含插入结点的指针,插入位置以及待插入关键值,待插入的子树指针。由于在分裂结点的创建已经对A[i]的父节点指针进行赋值,所以无需再对parent进行赋值操作。

具体的代码实现方式,

void insert_key(BTree q, int i, KeyType key, BTree ap)
{
    int k;

    for(k=q->keynum;k>i;k--)
    {
        q->ptr[k+1]=q->ptr[k];
        q->key[k+1]=q->key[k];
        q->recptr[k+1]=q->recptr[k];        
    }

    q->key[i+1]=key;
    q->ptr[i+1]=ap;
    q->recptr[i+1]=NULL;

   // if(ap)
    //{
        //ap->parent=q;
    //}

    q->keynum+=1;

    return;
}

上面提到了,在节点的插入过程中,可能会导致根节点分裂,根节点分裂需要建立根节点,同时对根节点进行重新赋值,所以我们需要涉及根节点分裂函数(创建函数)。

void create_new_root(BTree *bt, BTree q, KeyType x, BTree ap)
{
    BTree new_node;

    new_node=(BTree)malloc(sizeof(BTNode));

    new_node->ptr[0]=(*bt);
    new_node->key[1]=x;
    new_node->recptr[1]=NULL;
    new_node->ptr[1]=ap;
    new_node->parent=NULL;
    new_node->keynum=1;

    if(*bt)
    {
        (*bt)->parent = new_node;
    }

    if(ap)
    {
        ap->parent = new_node;
    }
   

    (*bt)=new_node;

    return;
}

最后我们来到节点插入函数,有了上述铺垫之后,节点插入函数实现就是水到渠成的事情了。过程中我们需要跟踪插入是否完成,如果插入后,结点中的关键字数量小于m,则插入完成;如果插入未完成,则需要不断向上迭代,不断搜索新的关键值和指针在上一级结点中插入的位置。

如果bt是空树或者根节点已经已经分裂为结点*q 和 *ap ,那么就需要调用根节点生成函数,重新建立新的根节点。

void insert_btree(BTree *bt, KeyType key, BTree q, int i)
{
    int         s;
    BTree       ap=NULL; //application pointer;
    KeyType     x;

    bool        finished=false;

    x=key;

    while(q && !finished)
    {
        insert_key(q,i,x,ap);

        if(q->keynum < m)
        {
            finished=true;
        }
        else
        {
            s=(m+2-1)/2;
            x=q->key[s];
            split_node(q,s,&ap);
            q=q->parent;
            
            if(q)
            {
                i = search_index(q, x);
            }

        }
    }

    if (!finished)
    {
        create_new_root(bt, q, x, ap); // create_new_root(BTree *bt, BTree q,KeyType x,BTree ap);
    }

    return;
}

2.4 B-树删除操作

删除操作是B-树中最复杂,最难实现的功能,它涉及到多种情况的判断,以及条件判断或可能产生的递归调用,代码实现过程中参考了《数据结构-C语言版》(严蔚敏,吴伟民版)课本源码+习题集解析使用说明 - 康建伟 - 博客园 (cnblogs.com)中的代码,并进行微小更改,在此表示感谢。

按照实际应用,B-树的删除分为三类情况,但由于涉及到相邻的两个子树,程序中会涉及到更多的判断情况。删除过程的出发点是最底层的非终端结点,如果待删除对象位于中间结点中,在执行删除操作之前,可以用它的直接后继节点进行替换后,再进行相关的删除。子函数实现分别为:

2.4.1 查找直接后继关键字

某个结点的后继,其实就是其相邻有节点所指向的子树上的最小值,对其子树为参数,寻找关键字的后继。实现代码相对比较简单,不断在p->ptr[0]中进行迭代,直至为空,并且把位置i赋值为1.

Result find_successor(BTree bt)
{
    Result res;
    BTree p;

    p=bt;

    while(p->ptr[0]!=NULL)
    {
        p=p->ptr[0];
    }

    res.pt=p;
    res.i=1;
    res.tag=1;

    return res;
}

如果待删除对象位于中间结点中,删除前需要做替换处理,函数实现如下:

void delete_btree(BTree *bt, KeyType key)
{
    int i;
    Result res;
    Result successor;
    BTree p;
    BTree q;
    
    res=search_btree(*bt,key);
    q=res.pt->ptr[res.i];

    if(q!=NULL)
    {
        successor = find_successor(q);
        res.pt->key[res.i]=successor.pt->key[successor.i];
        res.pt->recptr[res.i]=successor.pt->recptr[successor.i];
        res=successor;
    }

    delete_key(bt,res.pt,res.i);
    
    return;
}

最后让我们看一下大名鼎鼎的删除操作具体函数的实现,这个函数过程中,如果待删除对象,相邻左右子树的关键字数量都为[m/2]-1,那么就需要在这个过程中,就需要进行递归前虚拟一个关键字,然后进行递归操作。具体实现方式,请读者自行理解。

void delete_key(BTree *bt, BTree q, int i)
{
    int s; //seperate point
    int flag;
    int order;
    int j;

    BTree p;
    BTree rc;
    BTree lc;

    s=(m+2-1)/2;

    flag=0;
    order=-1;
    p=NULL;

    if(!find_parent(q,&p,&order))
    {
        flag=1; //only root node
    }
    else
    {
        if(q->keynum>=s)
        {
            flag=2; //remove the key directly
        }
        else
        {
            if (flag == 0 && order < p->keynum && p->ptr[order + 1]->keynum >= s)
                flag = 3; // 右兄弟关键字个数>=┌M/2┐

            if (flag == 0 && order > 0 && p->ptr[order - 1]->keynum >= s)
                flag = 4; // 左兄弟关键字个数>=┌M/2┐

            if (flag == 0 && order < p->keynum && p->ptr[order + 1]->keynum == (s - 1))
                flag = 5; // 右兄弟关键字个数==┌M/2┐-1

            if (flag == 0 && order > 0 && p->ptr[order - 1]->keynum == (s - 1))
                flag = 6; // 左兄弟关键字个数==┌M/2┐-1
        }
    }


   switch (flag)
   {
   case 1:
        if(q->keynum==1 && i==1)
        {
            *bt=q->ptr[0]; //*bt==NULL;
        }
        else
        {
            memcpy(q->key + i, q->key + i+1,sizeof(KeyType)*(q->keynum-i));
            memcpy(q->recptr + i, q->recptr + i + 1, sizeof(Record) * (q->keynum - i));
            memcpy(q->ptr + i, q->ptr + i + 1, sizeof(BTNode *) * (q->keynum - i));
            q->keynum=q->keynum-1;
        }
    break;

   case 2:
            memcpy(q->key + i, q->key + i + 1, sizeof(KeyType) * (q->keynum - i));
            memcpy(q->recptr + i, q->recptr + i + 1, sizeof(Record) * (q->keynum - i));
            memcpy(q->ptr + i, q->ptr + i + 1, sizeof(BTNode *) * (q->keynum - i));
            q->keynum = q->keynum - 1;
    break;
   case 3: //右兄弟关键字个数>=┌M/2┐
            rc = p->ptr[order + 1];
            memcpy(q->key + i, q->key + i + 1, sizeof(KeyType) * (q->keynum - i));
            memcpy(q->recptr + i, q->recptr + i + 1, sizeof(Record) * (q->keynum - i));
            memcpy(q->ptr + i, q->ptr + i + 1, sizeof(BTNode *) * (q->keynum - i));

            q->key[q->keynum]=p->key[order+1];
            q->recptr[q->keynum]=p->recptr[order+1];
            q->ptr[q->keynum]=rc->ptr[0];

            

            p->key[order+1]=rc->key[1];
            p->recptr[order+1]=rc->recptr[1];
            rc->ptr[0]=rc->ptr[1];

            memcpy(rc->key + 1, rc->key + 2, sizeof(KeyType) * (rc->keynum - 1));
            memcpy(rc->recptr + 1, rc->recptr + 2, sizeof(Record) * (rc->keynum - 1));
            memcpy(rc->ptr + 1, rc->ptr + 2, sizeof(BTNode *) * (rc->keynum - 1));
            rc->keynum = rc->keynum - 1;

            break;
   case 4: // 左兄弟关键字个数>=┌M/2┐
            lc = p->ptr[order - 1];

            for(j=i;j>=2;j--)
            {
                q->key[j]=q->key[j-1];
                q->ptr[j]=q->ptr[j-1];
                q->recptr[j]=q->recptr[j-1];
            }

            q->ptr[1]=q->ptr[0];
            q->key[1]=p->key[order];
            q->ptr[0]=lc->ptr[lc->keynum];
            p->key[order]=lc->key[lc->keynum];       
            lc->keynum--;
            break;
   case 5: // 右兄弟关键字个数==┌M/2┐-1
            rc=p->ptr[order+1];

            memcpy(q->key + i, q->key + i + 1, sizeof(KeyType) * (q->keynum - i));
            memcpy(q->recptr + i, q->recptr + i + 1, sizeof(Record) * (q->keynum - i));
            memcpy(q->ptr + i, q->ptr + i + 1, sizeof(BTNode *) * (q->keynum - i));

            q->key[q->keynum] = p->key[order + 1];
            q->recptr[q->keynum]=p->recptr[order+1];
            q->ptr[q->keynum] = rc->ptr[0];

            memcpy(q->key + q->keynum+1, rc->key+1, sizeof(KeyType) * (rc->keynum));
            memcpy(q->recptr + q->keynum + 1, q->recptr + 1, sizeof(Record) * (rc->keynum));
            memcpy(q->ptr + q->keynum + 1, q->ptr + 1, sizeof(BTNode *) * (rc->keynum));

            q->keynum += rc->keynum;

            memcpy(p->key + order + 1, p->key + order + 2, sizeof(KeyType) * (p->keynum-order-1));
            memcpy(p->recptr + order + 1, p->recptr + order + 2, sizeof(Record) * (p->keynum - order - 1));
            memcpy(p->ptr + order + 1, p->ptr + order + 2, sizeof(BTNode *) * (p->keynum - order - 1));

            p->keynum--;

            if (p->keynum < (s - 1))
            {
                p->keynum++; // 构造一个虚拟关键字
                q = p;
                delete_key(bt, q, q->keynum);
            }

            break;
   case 6: // 左兄弟关键字个数==┌M/2┐-1
            lc = p->ptr[order - 1];
            lc->key[lc->keynum + 1] = p->key[order];
            lc->recptr[lc->keynum + 1] = p->recptr[order];
            lc->ptr[lc->keynum + 1] = q->ptr[0];

            memcpy(lc->key + lc->keynum + 2, q->key+1, sizeof(KeyType) * (i-1));
            memcpy(lc->recptr + lc->keynum + 2, q->recptr + 1, sizeof(Record) * (i - 1));
            memcpy(lc->ptr + lc->keynum + 2, q->ptr + 1, sizeof(BTNode *) * (i - 1));

            memcpy(lc->key + lc->keynum + i + 1, q->key + i + 1, sizeof(KeyType) * (q->keynum - i));
            memcpy(lc->recptr + lc->keynum + i + 1, q->recptr + i + 1, sizeof(Record) * (q->keynum - i));
            memcpy(lc->ptr + lc->keynum + i + 1, q->ptr + i + 1, sizeof(BTNode *) * (q->keynum - i));

            lc->keynum += q->keynum;

            memcpy(p->key + order , p->key + order + 1, sizeof(KeyType) * (p->keynum - order));
            memcpy(p->recptr + order, p->recptr + order + 1, sizeof(Record) * (p->keynum - order));
            memcpy(p->ptr + order, p->ptr + order + 1, sizeof(BTNode *) * (p->keynum - order));

            p->keynum--;
            if (p->keynum < (s - 1))
            {
                p->keynum++; // 构造一个虚拟关键字
                q = p;
                delete_key(bt, q, q->keynum);
            }

            break;
   }
    

}
  1. 小结

通过代码回顾,更好认识到B-树操作实现的复杂性,尤其是对关键字的删除操作的函数实现,更是分为多种情况进行讨论,对于删除函数中,进行单边标记的编程技巧的学习,也是受益匪浅。

参考资料:

《数据结构-C语言版》(严蔚敏,吴伟民版)课本源码+习题集解析使用说明 - 康建伟 - 博客园 (cnblogs.com)


http://lihuaxi.xjx100.cn/news/1103426.html

相关文章

信奥一本通1242

1242&#xff1a;网线主管 时间限制: 1000 ms 内存限制: 65536 KB 提交数: 25297 通过数: 6122 【题目描述】 仙境的居民们决定举办一场程序设计区域赛。裁判委员会完全由自愿组成&#xff0c;他们承诺要组织一次史上最公正的比赛。他们决定将选手的电脑用星形拓扑…

实验一 进程管理与进程同步

实验一 进程管理与进程同步 实验目的&#xff1a; 了解进程管理的实现方法&#xff0c;理解和掌握处理进程同步问题的方法。 实验内容&#xff1a; 实现银行家算法、进程调度过程的模拟、读者-写者问题的写者优先算法。 实验步骤&#xff1a; 1.银行家算法流程图 &…

pytorch矩阵乘法总结

1. element-wise&#xff08;*&#xff09; 按元素相乘&#xff0c;支持广播&#xff0c;等价于torch.mul() a torch.tensor([[1, 2], [3, 4]]) b torch.tensor([[2, 3], [4, 5]]) c a*b # 等价于torch.mul(a,b) # tensor([[ 2, 6], # [12, 20]]) a * torch.tenso…

不废话!CentOS 8 安装docker的详细过程

目录 1.更新系统 2. 安装依赖包 3.添加 Docker YUM 仓库 4.安装 Docker 5.启动 Docker 6.设置 Docker 开机自启 7.测试 Docker 1.更新系统 dnf update 这里直接输入y&#xff0c;耐心等待更新即可 直到看到complete表示更新完毕 2. 安装依赖包 Docker 需要一些依赖包才能正常…

Django框架之模型自定义管理器

类属性 objects 是manager类的一个对象&#xff0c;作用是与数据库进行交互。 当定义模型类没有指定管理器&#xff0c;django会为模型创建objects管理器。 表结构与数据 CREATE TABLE myapp_grades (id int(11) NOT NULL AUTO_INCREMENT,name varchar(20) NOT NULL,boy_num…

Kali Linux 配置动态/静态 IP

[笔者系统版本] [Kali]: Kali Linux 2023.1 [Kernel]: kernel 6.1.0 [Desktop]: Xfce 4.18.1 1. Kali Linux 配置动态 IP (1). 首先查看网卡接口名称。 (2). 编辑网络接口配置文件。 (3). 网络接口配置文件的默认内容是这样的。 (4). 新增配置内容如下&#xff1b; 指定网卡…

【Linux】Linux学习之常用命令一

介绍 这里是小编成长之路的历程&#xff0c;也是小编的学习之路。希望和各位大佬们一起成长&#xff01; 以下为小编最喜欢的两句话&#xff1a; 要有最朴素的生活和最遥远的梦想&#xff0c;即使明天天寒地冻&#xff0c;山高水远&#xff0c;路远马亡。 一个人为什么要努力&a…

helm和chart

Helm helm是Kubernetes 应用的包管理工具&#xff0c;主要用来管理 Charts&#xff0c;类似Linux系统的yum。Helm Chart 是用来封装 Kubernetes 原生应用程序的一系列 YAML 文件。可以在你部署应用的时候自定义应用程序的一些 Metadata&#xff0c;以便于应用程序的分发。 he…