【C++高阶数据结构】B树、B+树、B*树

🏆个人主页企鹅不叫的博客

​ 🌈专栏

⭐️ 博主码云gitee链接:代码仓库地址

⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!

💙系列文章💙

【C++高阶数据结构】并查集

【C++高阶数据结构】图

【C++高阶数据结构】LRU



💎一、概念

🏆1.优点

B树适合外查找,当数据量很大,无法一次全部都放进内存的话,那就只能存在磁盘上,B树本质是一个多叉搜索树。

在这里插入图片描述

从树的根开始读取的话,我们需要读取树的高度次磁盘IO,多次进行磁盘读取,就会非常缓慢。每次要读取新的数据,要去定位这个过程是非常缓慢。

在平衡搜索树的基础上寻找优化方法
1.压缩高度,二叉变多插
2.一个结点里面存多行的值,也就是一个结点里面有多个关键字以及映射的值

🏆2.B树规则

一棵m阶(m>2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足一下性质:

  1. 根节点至少有两个孩子
  2. 每个分支节点都包含k-1个关键字和k个孩子,其中ceil(m/2) ≤ k ≤ m ceil是向上取整函数
  3. 每个叶子节点都包含k-1个关键字,其中 ceil(m/2) ≤ k ≤ m
  4. 所有的叶子节点都在同一层
  5. 每个节点中的关键字从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域划分
  6. 每个结点的结构为:(n,A0,K1,A1,K2,A2,… ,Kn,An)其中,Ki(1≤i≤n)为关键字,且Ki<Ki+1(1≤i≤n-1)。Ai(0≤i≤n)为指向子树根结点的指针。且Ai所指子树所有结点中的关键字均小于Ki+1。n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。

假设现在我们的m是10
按照上面的规则,也就是说我们
最少需要4个关键字和5个孩子
最多需要9个关键字和10个孩子

这个节点中的关键字按照从小到大的顺序进行排列
(n,A0,K1,A1,K2,A2,… ,Kn,An)
K1<K2<K3<…<Kn
A0节点中的值<K1<A1节点中的值<K2<……也就是说,如果比K1小,我们就在A0节点中继续寻找,如果比K2小比K1大,我们就在A1节点中进行查找。

B树本质是一个多叉搜索树。

💎二、B树插入

🏆1.插入过程

假设M=3。
也就是最少存1个关键字,最多两个关键字,最少2个孩子,最多3个孩子
这里是我们的数据:{53, 139, 75, 49, 145, 36, 101}

首先我们将53,139和75插入
(我们这里多开一个空间,便于我们的插入。否则我们的第m个元素插入的时候,也就是我们刚刚好越界的时候,我们就不知道插入在哪里,我们可能还要分情况进行讨论,这样就非常麻烦。多开辟一个空间的话,我们就可以先将这第M个元素先插进去,然后再进行分裂操作,就省去了分类讨论)

在这里插入图片描述

关键字的数量等于M,那就是满了,满了就分裂,分裂出一个兄弟(兄弟里面最初始没有值),然后分一半的值给兄弟

在这里插入图片描述

插入49,145

在这里插入图片描述

再插入36的时候,我们左边的结点1就满了,我们又需要进行分裂

在这里插入图片描述

49是我们的中位数。
所以我们将49放入我们的父节点中
(我们的关键字要比我们孩子的数量少一个。现在我们有三个孩子和两个关键字)

在这里插入图片描述

在这里插入图片描述

最右边的子树满了,进行持续分裂

在这里插入图片描述

B树天然平衡
因为它是向右和向上生长的
新插入的结点一定是在叶子插入的。叶子没有孩子,所以不会影响孩子和关键字的关系(孩子比关键字多一个)
叶子结点满了,就分裂出一个兄弟,提取中位数,向父亲插入一个值和一个孩子。
根节点分裂才会增加一层。

假设M=1024,那么一个4层的M路的B树可以存多少个值呢?
如果这棵树是全满的情况下
第一层1023个关键字,1024个孩子
第二层10241023个关键字(上一层的每一个孩子也就是这一层的每一个结点都有1023个关键字),10241024个孩子
第三层102410241023个关键字(上一层的每一个孩子也就是这一层的每一个结点都有1023个关键字),102410241024个孩子
第三层1024102410241023个关键字(上一层的每一个孩子也就是这一层的每一个结点都有1023个关键字),1024102410241024个孩子

最差的情况,最空的情况:
第一层只有1个关键字,2个孩子
第二层有2 * 512个关键字,大概1000个关键字,1000个孩子
第三层大概1000 * 512个关键字,1000 * 512个孩子
第四层大概50w * 512个关键字,约等于2.5亿个关键字

🏆2.代码实现

template<class K, size_t M>
struct BTreeNode {

    // 原本key是M-1个大小空间
    // 原本孩子是M个大小空间
    // 为了方便插入以后再分裂,多给一个空间
    K _keys[M];
    BTreeNode<K, M>* _subs[M + 1];
    BTreeNode<K, M>* _parent;
    size_t _n; // 记录实际存储关键字

    //初始化构造函数
    BTreeNode() {
        for (size_t i = 0; i < M; ++i) {
            _keys[i] = K();//缺省值
            _subs[i] = nullptr;
        }

        _subs[M] = nullptr;
        _parent = nullptr;
        _n = 0;
    }
};

// 数据是存在磁盘,K是磁盘地址,是M路的搜索树,我们的M是不确定的
template<class K, size_t M>
class BTree {
    typedef BTreeNode<K, M> Node;
public:
    //返回这个节点和下标
    pair<Node*, int> Find(const K& key) {
        Node* parent = nullptr;
        Node* cur = _root;

        while (cur) {
            // 在一个节点查找
            size_t i = 0;
            while (i < cur->_n) {
                if (key < cur->_keys[i]) {
                    break;
                }
                else if (key > cur->_keys[i]) {
                    ++i;
                }
                //找到了就返回这个节点
                else {
                    return make_pair(cur, i);
                }
            }

            // 往孩子去跳
            // 在往下一层跳之前先将当前的结点给parent
            parent = cur;
            cur = cur->_subs[i];
        }

        //找不到
        return make_pair(parent, -1);
    }

    void InsertKey(Node* node, const K& key, Node* child) {
        int end = node->_n - 1;
        while (end >= 0) {
            if (key < node->_keys[end]) {
                // 挪动key和他的右孩子
                node->_keys[end + 1] = node->_keys[end];
                node->_subs[end + 2] = node->_subs[end + 1];
                --end;
            }
            else {
                break;
            }
        }

        node->_keys[end + 1] = key;
        node->_subs[end + 2] = child;
        if (child) {
            child->_parent = node;
        }

        node->_n++;
    }

    //插入
    bool Insert(const K& key) {
        //第一次插入
        if (_root == nullptr) {
            //如果我们整颗树一个结点都没有
            _root = new Node;
            //将我们的第一个关键字传入
            _root->_keys[0] = key;
            _root->_n++;

            return true;
        }

        // key已经存在,不允许插入
        pair<Node*, int> ret = Find(key);
        if (ret.second >= 0) {
            return false;
        }

        // 如果没有找到,find顺便带回了要插入的那个叶子节点

        // 循环每次往cur插入 newkey和child
        Node* parent = ret.first;
        K newKey = key;
        Node* child = nullptr;
        while (1) {
            InsertKey(parent, newKey, child);
            // 满了就要分裂
            // 没有满,插入就结束
            if (parent->_n < M) {
                return true;
            }
            else {
                size_t mid = M / 2;
                // 分裂一半[mid+1, M-1]给兄弟
                Node* brother = new Node;
                size_t j = 0;
                size_t i = mid + 1;
                for (; i <= M - 1; ++i) {
                    // 分裂拷贝key和key的左孩子
                    brother->_keys[j] = parent->_keys[i];
                    brother->_subs[j] = parent->_subs[i];
                    if (parent->_subs[i]) {
                        parent->_subs[i]->_parent = brother;
                    }
                    ++j;

                    // 拷走重置一下方便观察
                    parent->_keys[i] = K();
                    parent->_subs[i] = nullptr;
                }

                // 还有最后一个右孩子拷给
                brother->_subs[j] = parent->_subs[i];
                if (parent->_subs[i]) {
                    parent->_subs[i]->_parent = brother;
                }
                parent->_subs[i] = nullptr;

                brother->_n = j;
                parent->_n -= (brother->_n + 1);

                K midKey = parent->_keys[mid];
                parent->_keys[mid] = K();


                // 说明刚刚分裂是根节点
                if (parent->_parent == nullptr) {
                    //创建一个新的父节点
                    _root = new Node;
                    _root->_keys[0] = midKey;
                    _root->_subs[0] = parent;
                    _root->_subs[1] = brother;
                    _root->_n = 1;

                    parent->_parent = _root;
                    brother->_parent = _root;
                    break;
                }
                else {
                    // 转换成往parent->parent 去插入parent->[mid] 和 brother
                    newKey = midKey;

                    child = brother;
                    parent = parent->_parent;
                }
            }
        }

        return true;
    }

    void _InOrder(Node* cur) {
        if (cur == nullptr)
            return;

        // 左 根  左 根  ...  右
        size_t i = 0;
        for (; i < cur->_n; ++i) {
            _InOrder(cur->_subs[i]); // 左子树
            cout << cur->_keys[i] << " "; // 根
        }

        _InOrder(cur->_subs[i]); // 最后的那个右子树
    }

    void InOrder() {
        _InOrder(_root);
    }

private:
    Node* _root = nullptr;
};

void TestBtree() {
    int a[] = { 53, 139, 75, 49, 145, 36, 101 };
    BTree<int, 3> t;
    for (auto e : a) {
        t.Insert(e);
    }
    t.InOrder();
}

时间复杂度:

第一层:M
第二层:M * M
第三层:M * M * M
第四层:M * M * M * M

N=M+M2 +M3 +M4 +……+Mh
h约等于
l o g M N log{M}^{N} logMN

💎三、B+树

🏆1.概念

1.分支节点的子树指针与关键字个数相同。(就相当于是取消掉了原先B树每个结点的最左边的那个孩子)
2.分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间
3.所有叶子节点增加一个链接指针链接在一起
4.所有关键字及其映射数据都在叶子节点出现】

5.节点的关键字数量是[1, M],非根节点关键字数量为[M/2,M]

6.分支节点跟叶子结点有重复的值,分支节点存的是叶子结点的索引
7.父亲中存的是孩子结点中的最小值做索引
8.分支节点可以只存key,叶子结点存key/value
在这里插入图片描述

🏆2.分裂过程

假设这是一棵M == 3的B+树,然后我们B+树要插入的数据是
{53,139,75,49,145,36,101,150,155};

1.依次插入53 139 75

在这里插入图片描述

2.插入19时发生裂变

在这里插入图片描述

在这里插入图片描述

3.插入146和36

在这里插入图片描述

4.插入101的时候发生第二次分裂

 [

在这里插入图片描述

5.插入150,插入155的时候发生连续的两次分裂

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

B+树的插入过程根B树是基本类似的,区别在于第一次插入的时候需要插入两层节点,一层做分支,一层做根,后面一样往叶子去插入,插入满了以后,分一半给兄弟,转换成往父亲插入一个key和一个孩子,孩子就是兄弟,key为兄弟结点的第一个最小值的key

总结:
1.简化孩子比关键字多一个的规则,变成相等。
2.所有值都在叶子上,方便便利查找所有值。

💎四、B*树

🏆1.概念

在这里插入图片描述

  • B树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2);
  • B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了)。
  • 如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。 所以,B * 树分配新结点的概率比B+树要低,空间使用率更高。
  • B*树主要就是节省空间。

💎五、B树系类应用

B树系列优点:

在内存中做内查找的话和哈希、平衡搜索树对比,单纯论树的高度,搜索效率而言,B树更好

B树系列缺点:

1.空间利用率低,消耗高
2.插入和删除数据、分裂和合并节点,那么必然挪动数据。
3.虽然高度更低,但是在内存中而言,跟哈希和平衡搜索树还是在一个量级的
结论:实质上B树系列在内存中体现不出优势。

B树系列的应用:数据库中的引擎MyISAM或者InnoDB

🏆1.MyISAM

MyISAM引擎是MySQL5.5.8版本之前默认的存储引擎,不支持事务,支持全文检索,使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址

在这里插入图片描述

yISAM中索引检索的算法为首先按 照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为 地址,读取相应数据记录。

🏆2.InnoDB

InnoDB存储引擎支持事务,InnoDB支持B+树索引、全文索引、哈希索引。InnoDB索引,表数据文件本身就是按B+Tree组织的一个索 引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。

在这里插入图片描述

先用name,name对应主键id,再用主键id再去搜索一次,也就是说他用索引查找需要查找两次

B树节点数据都在磁盘文件中。访问节点都是IO行为,只是他们会热数据缓存到Cache中


  • 11
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

penguin_bark

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值