1. 程式人生 > >淺談B-樹、B+樹

淺談B-樹、B+樹

B樹

資料庫的索引大多用B+樹實現,要了解B+樹,我們必須先了解什麼是B-樹?
首先要清楚的是,B-樹不能叫做B減樹,否則可就讓人笑掉大牙了,所以,後文中我們直接用作B樹。
之前我們講過,二叉搜尋樹的效率是O(log2^N),那為何資料庫中不用二叉搜尋樹來作為索引呢?此時我們必須考慮到磁碟IO。資料庫索引是儲存在磁碟上的,當資料量比加大 的時候,索引的大小可能有幾個G甚至更多。當我們利用索引查詢的時候,嗯呢該吧整個索引都載入到記憶體中嗎?顯然是不可能的,能做的只能是逐一載入每一個磁碟頁,這裡的磁碟頁對應著索引樹的節點。舉個栗子:
這裡寫圖片描述
要在上面這棵二叉搜尋樹中查詢10這個節點。
第一次IO:
這裡寫圖片描述


第二次IO:
這裡寫圖片描述
第三次IO:
這裡寫圖片描述
第四次IO:
這裡寫圖片描述
我們可以發現,在最壞的情況下,磁碟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比較:
這裡寫圖片描述
第二次磁碟IO,在記憶體中定位,和2,6比較:
這裡寫圖片描述
第三次磁碟IO,在記憶體中定位,和3,5比較:
這裡寫圖片描述
我們可以看出,B樹在查詢過程中的比較次數其實不比二叉查詢樹少,尤其當單一節點中的元素數量很多時。可是,相比於磁碟IO的速度,記憶體中的比較耗時幾乎可以忽略。所以只要樹的高度足夠低,IO次數足夠找,就可以提升效能。相比之下內部元素很多也沒有關係,僅僅是多了幾次記憶體互動,只要不超過磁碟頁大小即可。

B樹的插入:
B樹的插入只能在葉子節點,且當節點中的關鍵字滿了,要及逆行分裂操作。用上面的B樹舉例:
在葉子節點插入:
這裡寫圖片描述


第一次分裂:
這裡寫圖片描述
第二次分裂:
這裡寫圖片描述

B樹的刪除:
當刪除一個導致該樹不符合B樹的特性時,要進行左旋操作。比如,要刪除下面B樹的11這個節點,刪除後,12只有一個孩子,不符合B樹,此時,我們找出12,13,15這三個樹的中位數13,取代節點12,經過左旋12成為第一個孩子。
這裡寫圖片描述
這裡寫圖片描述
下面給出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樹主要應用於檔案系統以及部分資料庫索引,比如著名的非關係型資料庫MongoDB,而大部分關係型資料庫,比如Mysql,則使用B+樹作為索引。

B+樹
B+樹的大體特徵與B樹相似,但B+樹有自己的特性:
1.有k個子樹的中間節點包含有k個元素(B樹中是k-1個元素),每個元素不儲存資料,只用來索引,所有資料都儲存在葉子節點。
2.所有的葉子結點中包含了全部元素的資訊,及指向含這些元素記錄的指標,且葉子結點本身依關鍵字的大小自小而大順序連結。
3.所有的中間節點元素都同時存在於子節點,在子節點元素中是最大(或最小)元素。
這裡寫圖片描述
在B樹中,所有的節點都攜帶資料,但在B+樹中,只有葉子節點中有資料,中間節點僅僅是索引,沒有任何資料關聯。
由於B+樹的中間節點上沒有資料,所以,同樣大小的磁碟也可以容納更多的節點元素,這就意味著,資料量相同的情況下,B+樹的結構哦比B樹更加“矮胖”,因此查詢時IO的次數也更少。其次,B+樹的查詢必須查詢到葉子節點,而B樹是隻要找到匹配元素即可,無論是中間節點還是葉子節點,因此,B樹的查詢效能並不穩定,最好情況是查詢到根節點,最壞情況是查詢到葉子節點,而B+樹的查詢時穩定的,每一次都是查詢到葉子節點。B樹對節點的遍歷只能是繁瑣的中序遍歷,而B+樹的遍歷值需要對葉子節點的連結串列進行遍歷即可。

總結一下,B+樹相對於B樹的優勢有三個:
1.單一節點儲存更多的元素,使得查詢的IO次數更少。
2.所有查詢都要查詢到葉子節點,查詢效能穩定。
3.所有葉子節點形成有序連結串列,便於範圍查詢。