淺談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.所有葉子節點形成有序連結串列,便於範圍查詢。