【数据结构与算法】B_树

目录

前言:

一、B树

1、B树概念

2、B树查找

3、B树插入

4、B树前序遍历

5、B树性能

二、B+、B*树

1、B+树概念

2、B+树的插入

2、B*树概念

3、总结

三、B系列树的应用

总结


前言:

我们已经有很多索引的数据结构了

例如:

顺序查找 、二分查找 、二叉搜索树 、二叉平衡树(AVL树和红黑树) 、哈希
以上结构适合于数据量相对较小,能够一次性存放在内存中,进行数据查找
如果数据量很大,一次性无法放进内存中,那么只能存放到磁盘上,如果要进行搜索,只能将关键字映射的数据的地址存放到搜索树的节点中,要访问数据,就要先去磁盘中读取
磁盘的速度是远低于内存的,虽然平衡搜索树的时间复杂度是O(log H)(H是高度)
100亿个数据也就仅仅需要查找10次,但是这是在内存的情况,10次IO的速度和在内存中查找10次
的速度相差十分的大
哈希表虽然能够达到O(1)但是极端情况下哈希冲突非常严重,效率下降很多
所以为了解决大量数据的查询,在平衡搜索树的基础上提出了B树
B树主要进行了两点优化
1、压缩高度,二叉树变成多叉树
2、一个节点里面有多个关键字及映射的值

一、B树

1、B树概念

1970年,R.Bayer和E.mccreight提出了一种适用于外查找的,它是一种平衡的多叉树,称为B树(或B-树、B_树)。

一棵m阶B树(balanced tree of order m)是一棵平衡的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个孩子

向上取整的原因会在插入时说明

同时每一层的关键字按特定的顺序排列(升序,降序,可以使用仿函数来控制)

 

2、B树查找

还是以上图为例,现在要找53

寻找的思路与二叉搜索树类型,不过这次是在一个数组中寻找,

它既要在纵向上搜索,也要在横向上搜索

先在横向搜索,从左向右遍历找到与它的key相等的节点,如果不相等比它小,就继续向右遍历

比它小就到下一层寻找,直到找到该节点或者将整棵树遍历完都没有找到

为了提高效率,因为对于每一个节点来说,它是有序的,可以使用二分查找提高查找效率

同时为了方便实现Insert,我们将Find的返回值类型定义为pair<Node*, int>

如果能够找到就返回该节点的地址,及它在该节点位置的下标

找不到就返回它的父节点及-1,可以根据pair的second来判断是否找到该节点

找到它的父节点就可以方便进行插入了

   // 顺序遍历
    std::pair<Node *, int> Find(const K &key)
    {
        Node *cur = _root;
        Node *parent = nullptr;
        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 std::make_pair(cur, i);
                }
            }
            parent = cur;
            cur = cur->_subs[i];
        }
        return std::make_pair(parent, -1);
    }

    // 二分查找
    std::pair<Node *, int> Find(const K &key)
    {
        Node *cur = _root;
        Node *parent = nullptr;
        while (cur)
        {
            int left = 0;
            int right = cur->_n - 1;
            while (left <= right)
            {
                int mid = (left + right) >> 1;
                if (key < cur->_keys[mid])
                {
                    right = mid - 1;
                }
                else if (key > cur->_keys[mid])
                {
                    left = mid + 1;
                }
                else
                {
                    return std::make_pair(cur, mid);
                }
            }
            parent = cur;
            cur = cur->_subs[left];
        }
        return std::make_pair(parent, -1);
    }

3、B树插入

B树的插入,如果不考虑分裂是比较简单的

bool Insert(const K &key, const V &val)
    {
        if (_root == nullptr)
        {
            _root = new Node;
            _root->_keys[0] = key;
            _root->_val[0] = val;
            _root->_n++;

            return true;
        }

 

        return true;
    }

接下来看具体的插入过程

 

 

 

 

 

 

 

 

 

 

 

   void InsertKey(Node *node, const K &key, const V &val, Node *child)
    {
        int end = node->_n - 1;
        while (end >= 0)
        {
            if (key < node->_keys[end])
            {
                node->_keys[end + 1] = node->_keys[end];
                node->_val[end + 1] = node->_val[end];

                // child 也要对应上
                node->_subs[end + 2] = node->_subs[end + 1];

                end--;
            }
            else
            {
                break;
            }
        }

        // 先插入key
        node->_keys[end + 1] = key;
        node->_val[end + 1] = val;

        // 最后插入child,关键字比节点数少一
        node->_subs[end + 2] = child;
        if (child)
        {
            child->_parent = node;
        }
        node->_n++;
    }

   bool Insert(const K &key, const V &val)
    {
        if (_root == nullptr)
        {
            _root = new Node;
            _root->_keys[0] = key;
            _root->_val[0] = val;
            _root->_n++;

            return true;
        }

        // 插入节点过程
        K newKey = key;
        V newVal = val;
        Node *child = nullptr;
        std::pair<Node *, int> ret = Find(newKey);
        //说明已经存在该节点了,不用插入
        if (ret.second >= 0)
        {
            return false;
        }

        Node *parent = ret.first;
        while (true)
        {
            InsertKey(parent, newKey, newVal, child);
            if (parent->_n < M)
            {
                return true;
            }

            // B树满了,开始分裂,创建新节点,并且将原节点的一半拷贝给brother
            size_t mid = M / 2;
            Node *brother = new Node;
            size_t j = 0;
            size_t i = mid + 1;
            for (; i < M; i++)
            {
                brother->_keys[j] = parent->_keys[i];
                brother->_val[j] = parent->_keys[i];
                brother->_subs[j] = parent->_subs[i];

                // parent->child->parent = brother
                if (parent->_subs[i])
                {
                    parent->_subs[i]->_parent = brother;
                }

                // 处理干净,指针必须处理,val可以不处理
                parent->_keys[i] = K();
                parent->_val[i] = V();
                parent->_subs[i] = nullptr;

                j++;
            }
            // child比key多一个,处理最后的右子树
            brother->_subs[j] = parent->_subs[i];
            if (parent->_subs[i])
            {
                parent->_subs[i]->_parent = brother;
            }
            parent->_subs[i] = nullptr;

            brother->_n = j;
            parent->_n -= j + 1; // 还有一个节点给了parent

            K midKey = parent->_keys[mid];
            K midVal = parent->_val[mid];

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

            //说明刚才分裂的是根节点
            if (parent->_parent == nullptr)
            {
                _root = new Node;
                _root->_keys[0] = midKey;
                _root->_val[0] = midVal;
                _root->_subs[0] = parent;
                _root->_subs[1] = brother;
                _root->_n = 1;

                parent->_parent = _root;
                brother->_parent = _root;
                break;
            }
            else
            {
                // 转化为parent->parent 中插入 newKey和brother
                newKey = midKey;
                newVal = midVal;

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

        return true;
    }

4、B树前序遍历

它的前序遍历就是多叉树的前序遍历,同时不要忘记最后的右子树

   void _InOrder(Node *root)
    {
        if (root == nullptr)
        {
            return;
        }
        //左树,根,左树,根,左树,根……
        size_t i = 0;
        for (; i < root->_n; i++)
        {
            _InOrder(root->_subs[i]);
            std::cout << "Key  " << root->_keys[i] << " : "
                      << "val  " << root->_val[i] << std::endl;
        }

        //右数
        _InOrder(root->_subs[i]);
    }

    void InOrder()
    {
        _InOrder(_root);
    }

5、B树性能

对于一棵节点为N度为M的B-树,查找和插入需要log{M-1}N~log{M/2}N次比较,这个很好证明:
对于度为M的B-树,每一个节点的子节点个数为M/2 ~(M-1)之间,因此树的高度应该在要 log{M-1}Nlog{M/2}N之间,在定位到该节点后,再采用二分查找的方式可以很快的定位 到该元素
B-树的效率是很高的,对于N = 62*1000000000个节点,如果度M为1024,则log_{M/2}N <=
4,即在620亿个元素中,如果这棵树的度为1024,则需要小于4次即可定位到该节点,然后利用
二分查找可以快速定位到该元素,大大减少了读取磁盘的次数

二、B+、B*树

1、B+树概念

B+ 树是 B 树的变形,是在 B 树基础上优化的多路平衡搜索树, B+ 树的规则跟 B 树基本类似,但是又
B 树的基础上做了以下几点改进优化:
1、分支节点的子树指针与关键字个数相同
2、  分支节点的子树指针 p[i] 指向关键字值大小在 [k[i] k[i+1]) 区间之间
3、所有叶子节点增加一个链接指针链接在一起
4、  所有关键字及其映射数据都在叶子节点出现

总结:

分支节点跟叶子节点有重复的值,分支节点存的是叶子结点的索引

父亲中存的是孩子节点中的最小值做索引

B+树特性:

所有关键字都出现在叶子节点的链表中,且链表中的节点都是有序的

在分支节点无法获取value

分支节点相当于是叶子节点的索引,叶子节点才是存储数据的

 

2、B+树的插入

B+树的分裂:
当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增
加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向
兄弟的指针。

 

 

 

 

 

 

2、B*树概念

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

 

B*树主要是为了弥补B+树的空间利用率低的缺点

3、总结

通过以上介绍,大致将B树,B+树,B*树总结如下:
B树:有序数组+平衡多叉树;
B+树:有序数组链表+平衡多叉树;
B*树:一棵更丰满的,空间利用率更高的B+树

三、B系列树的应用

B-树最常见的应用就是用来做索引。索引通俗的说就是为了方便用户快速找到所寻之物,比如:
书籍目录可以让读者快速找到相关信息,hao123网页导航网站,为了让用户能够快速的找到有价
值的分类网站,本质上就是互联网页面中的索引结构。
MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构,简单来说:
索引就是数据结构
当数据量很大时,为了能够方便管理数据,提高数据查询的效率,一般都会选择将数据保存到数
据库,因此数据库不仅仅是帮助用户管理数据,而且数据库系统还维护着满足特定查找算法的数
据结构,这些数据结构以某种方式引用数据,这样就可以在这些数据结构上实现高级查找算法,
该数据结构就是索引。


总结


以上就是今天要讲的内容,本文仅仅简单介绍了B树的相关概念

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值