浅谈B-树、B+树

B树

数据库的索引大多用B+树实现,要了解B+树,我们必须先了解什么是B-树?
首先要清楚的是,B-树不能叫做B减树,否则可就让人笑掉大牙了,所以,后文中我们直接用作B树。
之前我们讲过,二叉搜索树的效率是O(log2^N),那为何数据库中不用二叉搜索树来作为索引呢?此时我们必须考虑到磁盘IO。数据库索引是存储在磁盘上的,当数据量比加大 的时候,索引的大小可能有几个G甚至更多。当我们利用索引查询的时候,嗯呢该吧整个索引都加载到内存中吗?显然是不可能的,能做的只能是逐一加载每一个磁盘页,这里的磁盘页对应着索引树的节点。举个栗子:
《浅谈B-树、B+树》
要在上面这棵二叉搜索树中查找10这个节点。
第一次IO:
《浅谈B-树、B+树》
第二次IO:
《浅谈B-树、B+树》
第三次IO:
《浅谈B-树、B+树》
第四次IO:
《浅谈B-树、B+树》
我们可以发现,在最坏的情况下,磁盘IO的次数等于这棵索引树的高度,为了减少磁盘IO的次数,我们需要让这棵树“降高度”,B树就是让这种“瘦高”的搜索树变成“矮胖”,从而减少磁盘IO的次数,提高搜索效率。

B树的性质
B树是一种用于外查找的多路平衡搜索树。
一棵M阶的B树:

1. 根节点至少有两个孩子,【2,M】
2. 每个非根节点有【M/2,M】个孩子
3. 每个非根节点有【M/2-1,M-1】个关键字,并且以升序排列
4. 每个节点孩子的数量比关键字的数量多一个
5. 所有的叶子节点都在同一层
6. key[i]和key[i+1]之间的孩子节点的值介于key[i]、key[i+1]之间

假如我们要加下面这棵B树中查找5这个节点:
第一次磁盘IO,在内存中定位,和9比较:
《浅谈B-树、B+树》
第二次磁盘IO,在内存中定位,和2,6比较:
《浅谈B-树、B+树》
第三次磁盘IO,在内存中定位,和3,5比较:
《浅谈B-树、B+树》
我们可以看出,B树在查询过程中的比较次数其实不比二叉查找树少,尤其当单一节点中的元素数量很多时。可是,相比于磁盘IO的速度,内存中的比较耗时几乎可以忽略。所以只要树的高度足够低,IO次数足够找,就可以提升性能。相比之下内部元素很多也没有关系,仅仅是多了几次内存交互,只要不超过磁盘页大小即可。

B树的插入:
B树的插入只能在叶子节点,且当节点中的关键字满了,要及逆行分裂操作。用上面的B树举例:
在叶子节点插入:
《浅谈B-树、B+树》
第一次分裂:
《浅谈B-树、B+树》
第二次分裂:
《浅谈B-树、B+树》

B树的删除:
当删除一个导致该树不符合B树的特性时,要进行左旋操作。比如,要删除下面B树的11这个节点,删除后,12只有一个孩子,不符合B树,此时,我们找出12,13,15这三个树的中位数13,取代节点12,经过左旋12成为第一个孩子。
《浅谈B-树、B+树》
《浅谈B-树、B+树》
下面给出B树的结构和插入操作:

#include<iostream>
using namespace std;

template<class K, class V, size_t M>
struct BTreeNode
{
    //多开一个空间,方便分裂
    pair<K, V> _kvs[M];//关键字数组
    BTreeNode<K, V, M>* _subs[M+1];//孩子节点
    BTreeNode<K, V, M>* _parent;//三叉
    size_t size;

    BTreeNode()
        :_parent(NULL)
        , size(0)
    {
        for (size_t i = 0; i < M+1; ++i)
        {
            _subs[i] = NULL;
        }
    }
};

template<class K,class V,size_t M>
class BTree
{
    typedef BTreeNode<K, V, M> Node;
public:
    BTree()
        :_root(NULL)
    {}

    pair<Node*, int> Find(const K& key)
    {
        //要返回这个节点和在这个节点中的位置
        Node* cur = _root;
        Node* parent = NULL;
        while (cur)
        {
            size_t i = 0;
            while (i < cur->size)
            {
                //在当前位置的左树
                if (cur->_kvs[i].first > key)
                    break;
                else if (cur->_kvs[i].first < key)
                {
                    ++i;
                }
                else
                    return make_pair(cur, i);
            }
            //在左树或是没找到
            parent = cur;
            cur = cur->_subs[i];
        }
        return make_pair(parent, -1);
    }

    void InSertKV(Node* cur, const pair<K, V>& kv, Node* sub)
    {
        int end = cur->size - 1;
        while (end >= 0)
        {
            if (cur->_kvs[end].first > kv.first)
            {
                //左子树的下标是与当前节点下标相同,右子树的下标是当前节点坐标+1
                cur->_kvs[end + 1] = cur->_kvs[end];
                cur->_subs[end + 2] = cur->_subs[end + 1];
                --end;
            }
            else
            {
                break;
            }
        }
        //end<0或kv.first>cur_kvs[end].first
        cur->_kvs[end + 1] = kv;
        cur->_subs[end + 2] = sub;
        if (sub)
            sub->_parent = cur;
        cur->size++;
    }

    Node* Divided(Node* cur)
    {
        Node* newNode = new Node;
        int mid = (cur->size) / 2;
        size_t j = 0;
        size_t i = mid + 1;
        for (; i < cur->size; ++i)
        {
            newNode->_kvs[j] = cur->_kvs[i];
            newNode->_subs[j] = cur->_subs[i];
            if (newNode->_subs[j])
                newNode->_subs[j]->_parent = newNode;
            newNode->size++;
            j++;
        }
        //右孩子还没拷
        newNode->_subs[j] = cur->_subs[i];
        if (newNode->_subs[j])
            newNode->_subs[j]->_parent = newNode;
        return newNode;
    }

    bool InSert(const pair<K, V>& kv)
    {
        //节点为NULL直接插入
        if (_root == NULL)
        {
            _root = new Node;
            _root->_kvs[0] = kv;
            _root->size = 1;
            return true;
        }
        //找到相同值返回false,没找到返回true,节点的关键字满了就进行分裂
        pair<Node*, int> ret = Find(kv.first);
        if (ret.second >= 0)
            return false;

        //没找到,可以插入节点
        Node* cur = ret.first;
        pair<K, V> newKV = kv;//新的关键字
        Node* sub = NULL;

        while (1)
        {
            //插入一个人孩子和一个关键字
            InSertKV(cur, newKV, sub);
            if (cur->size < M)
                return true;
            else
            {
                //需要分裂
                Node* newNode = Divided(cur);
                pair<K, V> midKV = cur->_kvs[(cur->size) / 2];
                //根节点分裂
                cur->size -= (newNode->size + 1);
                if (cur == _root)
                {
                    _root = new Node;
                    _root->_kvs[0] = midKV;
                    _root->size = 1;
                    _root->_subs[0] = cur;
                    _root->_subs[1] = newNode;
                    cur->_parent = _root;
                    newNode->_parent = _root;
                    return true;
                }
                else
                {
                    sub = newNode;
                    newKV = midKV;
                    cur = cur->_parent;
                }
            }
        }
    }

    void InOrder()
    {
        _InOrder(_root);
    }

protected:
    void _InOrder(Node* root)
    {
        if (root == NULL)
            return;
        Node* cur = root;
        size_t i = 0;
        for (; i < cur->size; ++i)
        {
            _InOrder(root->_subs[i]);
            cout << cur->_kvs[i].first << " ";
        }
        _InOrder(cur->_subs[i]);
    }
private:
    Node* _root;
};

void Test()
{
    int a[] = { 53, 75, 139, 49, 145, 36, 101 };
    int sz = sizeof(a) / sizeof(a[0]);
    BTree<int, int, 3>bt;
    for (size_t i = 0; i < sz; ++i)
    {
        bt.InSert(make_pair(a[i],i));
    }
    bt.InOrder();
}

《浅谈B-树、B+树》

B树主要应用于文件系统以及部分数据库索引,比如著名的非关系型数据库MongoDB,而大部分关系型数据库,比如Mysql,则使用B+树作为索引。

B+树
B+树的大体特征与B树相似,但B+树有自己的特性:
1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
《浅谈B-树、B+树》
在B树中,所有的节点都携带数据,但在B+树中,只有叶子节点中有数据,中间节点仅仅是索引,没有任何数据关联。
由于B+树的中间节点上没有数据,所以,同样大小的磁盘也可以容纳更多的节点元素,这就意味着,数据量相同的情况下,B+树的结构哦比B树更加“矮胖”,因此查询时IO的次数也更少。其次,B+树的查询必须查找到叶子节点,而B树是只要找到匹配元素即可,无论是中间节点还是叶子节点,因此,B树的查找性能并不稳定,最好情况是查找到根节点,最坏情况是查找到叶子节点,而B+树的查询时稳定的,每一次都是查找到叶子节点。B树对节点的遍历只能是繁琐的中序遍历,而B+树的遍历值需要对叶子节点的链表进行遍历即可。

总结一下,B+树相对于B树的优势有三个:
1.单一节点存储更多的元素,使得查询的IO次数更少。
2.所有查询都要查找到叶子节点,查询性能稳定。
3.所有叶子节点形成有序链表,便于范围查询。

    原文作者:B树
    原文地址: https://blog.csdn.net/ChaseRaod/article/details/78631492
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞