B-Tree和B+Tree学习笔记

B-Tree和B+Tree学习笔记

1. B-Tree

1.1 定义

全称Balance-tree(平衡多路查找树)平衡的意思是左边和右边分布均匀。多路的意思是相对于二叉树而言的,二叉树是二路查找树,而B-tree有多条路。


B-Tree的阶指的是节点最大的孩子数目。

我的理解是在二叉树的基础上增加了规则:

  1. 若根节点非叶子节点,则其至少有两棵子树
  2. 除了根节点和叶子节点之外,每个节点有k-1个key和k个孩子,其中**m/2上取整-1 ≤ k ≤ m-1**
  3. 所有叶子节点在同一层次
  4. key按序排列
1.2 图例

如下图是一个阶m=3的B-Tree:

3阶B树

1.3 用途

显著减少定位记录时经历的过程。一般用于数据库的索引

因为B树相对AVL树之类的结构而言具有一个节点存多个数据以及较为矮胖的特性,所以可以显著降低访问磁盘带来的IO开销。

对比B树和平衡二叉树,是否因为显著减少层高,减少了磁盘IO?

1.4 代码实现
1.4.1 数据结构定义
  1. 节点类型

    所有非终端节点中包含(n,A0,K1,A1,K2,A2…,Kn,An)。

    其中n为关键字个数,Ki为关键字,Ai-1为小于Ki的节点指针,Ai为大于Ki的节点指针。

    // 结点类型
    typedef struct BTNode
    {
        int keynum;                //结点关键字个数
        struct BTNode *parent;     //指向双亲结点
        KeyType key[m + 1];        //关键字数组,0号单元未用
        struct BTNode *ptr[m + 1]; //孩子结点指针数组
        Record recptr[m + 1];      //记录数组,0号单元未用
        BTNode() : keynum(), parent(), key(), ptr(), recptr() {}
    } BTNode, *BTree;
    
  2. 查找结果类型

    用于记录在B-Tree中的查找结果。

    // 查找结果类型
    struct Result
    {
        BTNode *pt; //指向找到的结点
        int i;      //在结点中的关键字位置;
        bool found; //查找成功与否标志
        Result() : pt(), i(), found() {}
        Result(BTNode *pt, int i, bool found) : pt(pt), i(i), found(found) {}
    };
    
1.4.2 实现查找
  1. SearchBTNode()函数

    在结点p中查找关键字k的位置 i

    **i **使得:p->key[i] <= k < p->key[i+1]

    找到对应key则 i 就是key的下标,没找到对应key则 i 就是继续向下查找的指针的下标

    int SearchBTNode(BTNode *p, KeyType k)
    {
        int i = 0;
        while (i < p->keynum && k >= p->key[i + 1])
            i++;
        return i;
    }
    
  2. SearchBTree()函数

    在树t上查找关键字k,返回结果(pt,i,tag)

    成功则tag=1,关键字k是指针pt所指结点中第i个关键字

    失败则tag=0,关键字k的插入位置为pt结点的第i个

    Result SearchBTree(BTree t, KeyType k)
    {
        // 初始化,p指向待查节点,q指向p的双亲
        BTNode *p = t;
        BTNode *q = nullptr;
        bool found = false;
        int i = 0;
        //while循环查找过程,条件1:p非空(没到叶子节点),条件2:还没找到
        while (p && !found)
        {
            i = SearchBTNode(p, k);
            if (i > 0 && p->key[i] == k)
                found = true;
            else
            {
                q = p;
                p = p->ptr[i];
                found = false;
            }
        }
        if (found)
            return Result(p, i, 1); // 查找成功,返回其在B树中位置
        else
            return Result(q, i, 0); // 查找不成功,返回其插入位置
    }
    
1.4.3 实现插入
  1. InsertBTNode()函数

    将关键字k和结点q分别插入到p->key[i+1]和p->ptr[i+1]中

    void InsertBTNode(BTNode *&p, int i, KeyType k, BTNode *q)
    {
        // i+1之后(包括原i+1)整体后移,空出位置i+1
        int j;
        for (j = p->keynum; j > i; j--)
        {
            p->key[j + 1] = p->key[j];
            p->ptr[j + 1] = p->ptr[j];
        }
        // 插入k和q
        p->key[i + 1] = k;
        p->ptr[i + 1] = q;
        if (q != nullptr)
            q->parent = p;
        p->keynum++;
    }
    
  2. SplitBTNode()函数

    将插入新节点后的节点p(含有m个key,已经节点过大)分裂成两个结点,并返回分裂点x

    KeyType SplitBTNode(BTNode *&p, BTNode *&q)
    {
        int s = (m + 1) / 2;
        q = new BTNode;        //新建节点q
        q->ptr[0] = p->ptr[s]; //后一半移入节点q
        for (int i = s + 1; i <= m; i++)
        {
            q->key[i - s] = p->key[i];
            q->ptr[i - s] = p->ptr[i];
            q->recptr[i - s] = p->recptr[i];
        }
        q->keynum = p->keynum - s; //节点q存放m-s个
        q->parent = p->parent;
        //转移到节点q之后,修改双亲指针
        for (int i = 0; i <= q->keynum; i++)
        {
            if (q->ptr[i] != nullptr)
                q->ptr[i]->parent = q;
        }
        p->keynum = s - 1; //节点p存放s个,但保留s-1个,p中最后一个key作为分裂点x
        return p->key[s];
    }
    
  3. NewRoot()函数

    当 【t为空树】或 【原根节点p分裂为p和q】 时使用

    生成新的根结点t,原结点p和结点q为子树指针

    void NewRoot(BTree &t, KeyType k, BTNode *p, BTNode *q)
    {
        t = new BTNode;
        t->keynum = 1;
        t->ptr[0] = p;
        t->ptr[1] = q;
        t->key[1] = k;
        // 调整结点p和结点q的双亲指针
        if (p != nullptr)
            p->parent = t;
        if (q != nullptr)
            q->parent = t;
        t->parent = nullptr;
    }
    
  4. InsertBTree()函数

    在树t上结点p的key[i]之后插入关键字k

    若引起结点过大,则尝试向上分裂

    Status InsertBTree(BTree &t, int i, KeyType k, BTNode *p)
    {
        bool finished = false;
        BTNode *q = nullptr;
        KeyType x = k;
        while (p && !finished)
        {
            InsertBTNode(p, i, x, q);
            if (p->keynum < m)
                finished = true;
            else
            {
                x = SplitBTNode(p, q);
                //若p的双亲存在,在双亲节点中找x的插入位置
                p = p->parent;
                if (p)	i = SearchBTNode(p, x);
            }
        }
        //未完成,情况1:t是空树(初始p为nullptr),情况2:根节点分裂为p和q
        if (!finished)
        {
            p = t; //原t即为p
            NewRoot(t, x, p, q);
        }
        return OK;
    }
    
1.4.4 实现删除
  1. AdjustBTree()函数

    删除节点p中的第i个关键字后,调整B树

    在删除操作后,如果非根节点出现节点关键字个数不足的情况,会使用AdjustBTree()函数来调整B树。

    假设删除的key为k,它所在节点的双亲节点为p

    • k在p的最左孩子节点中:
      • 右兄弟够借:p的最左key移入k所在节点,右兄弟的最左key移入p。
      • 右兄弟不够借:将p中的Ki和右兄弟中的全部key一起合并到k所在节点。
    • k在p的最右孩子节点中:
      • 左兄弟够借:p的最右key移入k所在节点,左兄弟的最右key移入p。
      • 左兄弟不够借:将p中的Ki和k所在节点剩余的key一起合并到左兄弟。
    • k在p的中间孩子节点中:
      • 左兄弟够借:p的最右key移入k所在节点,左兄弟的最右key移入p。
      • 右兄弟够借:p的最左key移入k所在节点,右兄弟的最左key移入p。
      • 都不够借:将p中的Ki和k所在节点剩余的key一起合并到左兄弟。
    void AdjustBTree(BTNode *p, int i)
    {
        // 删除的是最左孩子节点中的关键字
        if (i == 0)
            if (p->ptr[1]->keynum > keyNumMin) //右节点够借
                MoveLeft(p, 1);
            else //右节点不够借
                Combine(p, 1);
        // 删除的是最右孩子节点中的关键字
        else if (i == p->keynum)
            if (p->ptr[i - 1]->keynum > keyNumMin) //左节点够借
                MoveRight(p, p->keynum);
            else //左节点不够借
                Combine(p, p->keynum);
        // 删除的是中间孩子节点中的关键字且左节点够借
        else if (p->ptr[i - 1]->keynum > keyNumMin)
            MoveRight(p, i);
        // 删除的是中间孩子节点中的关键字且右节点够借
        else if (p->ptr[i + 1]->keynum > keyNumMin)
            MoveLeft(p, i + 1);
        // 删除的是中间孩子节点中的关键字且左右都不够借
        else
            Combine(p, i);
    }
    
  2. BTNodeDelete()函数

    在节点p中查找并删除关键字k,若未找到则递归向下删除

    bool BTNodeDelete(BTNode *p, KeyType k)
    {
        int i;
        bool found; //查找标志
        if (p == nullptr)
            return 0;
        else
        {
            found = FindBTNode(p, k, i); //返回查找结果
            //查找成功
            if (found)
            {
                //删除的是非叶子节点的关键字
                //理解i-1:若为非叶子节点,被删除关键字为key[i],则ptr[i-1]一定存在
                if (p->ptr[i - 1] != nullptr)
                {
                    p->key[i] = FindReplace(p->ptr[i]); //寻找相邻关键字(右子树中最小的关键字)
                    BTNodeDelete(p->ptr[i], p->key[i]); //执行删除操作
                }
                else
                    Remove(p, i); //从节点p中位置i处删除关键字
            }
            else
                found = BTNodeDelete(p->ptr[i], k); //沿孩子节点递归查找并删除关键字k
            // 非叶子节点删除后可能从右子树替补,可能会使右子树关键字个数不足
            if (p->ptr[i] != nullptr)
                if (p->ptr[i]->keynum < keyNumMin) //删除后关键字个数不足
                    AdjustBTree(p, i);             //调整B树
            return found;
        }
    }
    
  3. BTreeDelete()函数

    删除操作,在B树中删除关键字k

    调用BTNodeDelete()函数

    void BTreeDelete(BTree &t, KeyType k)
    {
        bool deleted = BTNodeDelete(t, k); //删除关键字k
        //查找失败
        if (!deleted)
            printf("key[%d] is not exist!\n", k);
        //删除后根节点无key
        else if (t->keynum == 0)
        {
            BTNode *p = t;
            t = t->ptr[0];
            delete p;
        }
    }
    
  4. DestroyBTree()函数

    递归释放B树

    void DestroyBTree(BTree &t)
    {
        if (!t)
            return;
        BTNode *p = t;
        for (int i = 0; i <= p->keynum; i++)
            DestroyBTree(p->ptr[i]);
        delete p;
        t = nullptr;
    }
    
1.4.5 实现层序遍历

这部分主要使用队列进行广度优先搜索,完成层序遍历,打印B树

重点是使用length记录每次的队列长度,控制每轮循环打印的节点个数刚好等于这一层的节点数

  1. LevelTraverse()函数

    层序遍历

    void LevelTraverse(BTree t)
    {
        queue<BTNode *> que;
        BTNode *p;
        int length;    //队列长度,用于控制每一层的节点个数
        int level = 0; //记录层数
    
        que.push(t);
        while (!que.empty())
        {
            length = que.size(); // 获取当前队列长度,用于分层
            //打印当前层所有节点
            printf("\tLevel %-2d:", level++);
            for (int i = 0; i < length; i++)
            {
                // 弹出头节点作为当前节点
                p = que.front();
                que.pop();
                // 打印当前节点
                printf("[");
                printf("%d", p->key[1]);
                for (int j = 2; j <= p->keynum; j++)
                {
                    printf(", %d", p->key[j]);
                }
                printf("]  ");
                // 把当前节点的所有非空子节点加入队列
                for (int j = 0; j <= p->keynum; j++)
                {
                    if (p->ptr[j] && p->ptr[j]->keynum != 0)
                        que.push(p->ptr[j]);
                }
            }
            printf("\n");
        }
    }
    
  2. PrintBTree()函数

    打印B树

    Status PrintBTree(BTree t)
    {
        if (t == NULL)
        {
            printf("\tEmpty B-Tree!\n");
            return EMPTY;
        }
        LevelTraverse(t);
        return OK;
    }
    
1.4.6 测试

测试B树各项功能

测试流程:

  1. 创建一棵m阶(m=5)B树,其中的key从1到16
  2. 打印菜单,可选操作有:
    • Init 初始化B树
    • Insert 插入key
    • Delete 删除key
    • Destroy 销毁B树
    • Exit 退出程序
  3. 进行操作观察输出

如下图是m=5,key从1到16的B树:

测试

2. B+Tree

2.1 B-Tree的不足
  1. 遍历时使用中序遍历,往返于节点之间,增加磁盘块读写开销
  2. 非叶子节点同时存储key和data,一个节点能存放的key数目受限,使树长高
  3. 查询性能不稳定
  4. 范围查询不友好
2.2 B+Tree的特点
  1. 叶子节点包含全部关键字
  2. 叶子节点之间用指针串接,遍历时直接从叶子节点的头指针开始
  3. 非叶子节点只存储key,data全部存放在叶子节点
  4. 所有非叶子节点可以看作索引,只存放其孩子的最大(或最小)key
  5. 查询性能稳定
  6. 范围查询友好
2.3 图例

如下图是一个阶m=4的B+Tree:

4阶BPlus树

图中非叶子节点存放的是其孩子的最小key,图左下的磁盘块4有问题,key=10的索引不应该出现在这个磁盘块里。

3. 对比B-Tree和B+Tree

3.1 差别
B-TreeB+Tree
n个关键字n+1个分支n个分支
节点内关键字个数[m/2(上取整)-1,m-1],根节点[1,m-1][m/2(上取整),m],根节点[2,m]
叶子节点关键字不全包含全部关键字,指针指向记录
非叶子节点含data不含data,只存索引
叶子节点链表
3.2 图例

对比B和BPlus

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值