B-樹的基本概念以及程式碼實現
B-樹引入
當我們從一堆資料裡查詢某個資料的時候,常使用如下方法:
資料雜亂無規律—>線性搜尋 —-> O(N)
資料有序—->二分查詢—->O(log2N)—>最差情況下退化成單隻樹O(N)
二叉搜尋樹/AVL樹/紅黑樹—->O(log2N)
其中二叉搜尋樹、 AVL樹、 紅黑樹都是動態查詢樹, 典型的二叉搜尋樹結構,查詢的時間複雜度和樹的高度相關O(log2N)。
這幾種樹的結構,很大程度上,提高了資料的查詢效率,但是資料一般儲存在磁碟上,若資料量過大不能全部載入到記憶體,那麼將導致這幾種資料結構的樹的高度太高, 增大訪問磁碟的次數, 從而導致效率低下。為了訪問所有資料, 使用如下搜尋樹結構儲存資料: 樹的結點中儲存權值(關鍵字)和磁碟的地址
由此,我們引入B-樹。
B-樹定義
1970年, R.Bayer和E.mccreight提出了一種適合外查詢的樹, 它是一種平衡的多叉樹, 稱為B樹。( 有些地方寫的是B-樹,注意不要誤讀成”B減樹”)一棵M階(M>2)的B樹, 是一棵平衡的M路平衡搜尋樹, 可以是空樹或者滿足一下性質:
1. 根節點至少有兩個孩子——假設根節點只有一個關鍵字,那麼至少有兩個關鍵字(一個大於根,一個小於根)存在,作為其孩子。
2. 每個非根節點至少有M/2(上取整)個孩子,至多有M個孩子——假設M為3,則該節點至少有(3/2+1)個孩子,至多有3個孩子。
3. 每個非根節點至少有M/2-1(上取整)個關鍵字,至多有M-1個關鍵字, 並且以升序排列
4. key[i]和key[i+1]之間的孩子節點的值介於key[i]、 key[i+1]之間——B樹為有序樹,每兩個鍵值之間的所有孩子節點的鍵值大小必然介於兩雙親節點之間。
5. 所有的葉子節點都在同一層——B樹不同於其他樹從上向下生長,而是自下而上,層層分裂。
- 關於性質,從圖解中詳細介紹。
圖解B樹
以下所有圖M值取3。
- 樹為空時
插入20時,對已有鍵值10和20進行比較,按照從左到右從小到大的順序插入。
當20插入根節點以後,節點size等於M,此時需要對節點進行分裂。若不分裂,則該節點孩子為四個,違反了性質2。
這裡節點結構定義時多給了一格,以便插入鍵值時鍵值陣列不會越界。
分裂過程如下圖:
分裂時,建立兩個新節點,一個作為根節點用以存放節點中間鍵值為20的節點,一個用來存放中間鍵值右邊的所有鍵值,其次,更新孩子雙親指向關係。
- 樹不空
依次插入40和50,自上而下查詢插入位置,根據大小排列,插入30所在節點,50插入後需要再次分裂節點,此時,因該節點非根節點,則,分裂時,將中間鍵值之後的鍵值移入新節點中,中間鍵值存入雙親節點中,在此例中,其雙親為根節點,往雙親插入中間鍵值時,按照從左向右,從小到大的順序,即鍵值的插入順序。
再次插入80,70。圖示如下:
分裂圖示如下:
上圖中,70插入後,該節點需要分裂,分裂完畢之後,70存入根節點,此時根節點也需要分裂,以滿足B樹性質。需要注意的是,分裂過程中,各個節點的孩子雙親指向需要及時更改,否則出錯,具體細節見程式碼實現。
B樹程式碼實現
#include<iostream>
using namespace std;
template <typename K, size_t M>
struct BTreeNode
{
K _keys[M]; // 關鍵字的集合——鍵值陣列 -->多出的一格防止陣列越界
BTreeNode* _pSons[M + 1]; // 孩子節點的集合-->多出的一格備用
BTreeNode* _pParent; // 雙親節點
size_t _size; // 有效關鍵字的個數——當前節點內當前關鍵字的數目
BTreeNode() // 建構函式-->對各成員進行初始化,初始時size為0,雙親節點為空
: _size(0)
, _pParent(NULL)
{
size_t i = 0;
for (i = 0; i < M; i++)
_pSons[i] = NULL; // 初始化前M個孩子為空
_pSons[i] = NULL; // 備用的那一格置空
}
};
template <typename K, size_t M>
class BTree
{
public:
typedef BTreeNode<K, M> Node; // 型別重新命名
BTree()
:_pRoot(NULL)
{}
// pair類由C++庫提供,它將一對值配對,這可能是不同型別(T1和T2)。可通過其第一和第二公共成員訪問。
pair<Node*, int> Find(const K& key) // 查詢鍵值為key的節點,返回該節點以及節點內位置下標
{
Node* pCur = _pRoot;
Node* pParent = NULL;
// 只要沒找到且pCur不為空,繼續查詢
while (pCur)
{
// 從根節點找起,只要下標i不越界且該位置的鍵值小於key,就繼續向後查詢。若大於key則跳出
size_t i = 0;
while (i < pCur->_size)
{
if (key < pCur->_keys[i])
break;
else if (key > pCur->_keys[i])
i++;
else
return pair<Node*, int>(pCur, i);
}
// pParent記錄pCur為空時的雙親節點
pParent = pCur;
pCur = pCur->_pSons[i];
}
// 未找到——>返回pParent,位置返回-1
return pair<Node*, int>(pParent, -1);
}
bool Insert(const K& key) //插入
{
// 若樹為空,直接插入,更新keys,size
if (_pRoot == NULL)
{
_pRoot = new Node;
_pRoot->_keys[0] = key;
_pRoot->_size = 1;
return true;
}
//找插入位置,若要插入的鍵值已存在,返回false
pair<Node*, int> pos = Find(key);
if (pos.second >= 0)
return false;
//插入
Node* pCur = pos.first; // 要插入的位置的坐在節點
Node* pSon = NULL; // 標誌pCur位置上新的孩子
K k = key;
// 迴圈檢查樹是否正確,對其及時進行調整,直到插入成功返回true
while (true)
{
// 在pCur節點裡插入鍵值k
InsertKey(pCur, pSon, k);
// 插入後若pCur的size<M,說明節點不需要分裂,直接返回
if (pCur->_size < M)
return true;
// 分裂節點
size_t mid = pCur->_size >> 1;
Node* newNode = new Node;
// 搬移mid右邊鍵值到新節點newNode,且更新搬移鍵值的孩子的指向
size_t i = 0;
for (i = mid+1; i < pCur->_size; i++)
{
newNode->_keys[newNode->_size] = pCur->_keys[i];
newNode->_pSons[newNode->_size++] = pCur->_pSons[i];
if (pCur->_pSons[i])
pCur->_pSons[i]->_pParent = newNode;
}
newNode->_pSons[newNode->_size] = pCur->_pSons[i];
if (pCur->_pSons[i])
pCur->_pSons[i]->_pParent = newNode;
// 更新pCur的size
pCur->_size = pCur->_size - newNode->_size - 1;
// 若pCur已經調整到根節點還未合格,則再次分裂,更新根節點後直接返回true
if (_pRoot == pCur)
{
_pRoot = new Node;
_pRoot->_keys[0] = pCur->_keys[mid];
_pRoot->_size = 1;
// 更新新的根節點的孩子以及孩子雙親的指向
_pRoot->_pSons[0] = pCur;
pCur->_pParent = _pRoot;
_pRoot->_pSons[1] = newNode;
newNode->_pParent = _pRoot;
return true;
}
else // 若pCur不為根,且仍舊不平衡,則pCur向上更新即指向其雙親,pSon指向新分裂出來的節點,並更新需要調整的鍵值
{
k = pCur->_keys[mid];
pCur = pCur->_pParent;
pSon = newNode;
}
}
}
// 中序遍歷
void InOrder()
{
cout << "InOrder:" << endl;
_InOrder(_pRoot);
cout << endl;
}
protected:
void _InOrder(Node* pRoot)
{
if (pRoot)
{
int i = 0;
for (; i < pRoot->_size; i++)
{
_InOrder(pRoot->_pSons[i]);
cout << pRoot->_keys[i] << " ";
}
_InOrder(pRoot->_pSons[pRoot->_size]); // 處理該節點最右邊的孩子
}
}
void InsertKey(Node* pCur, Node* pSon, const K& key)
{
int end = pCur->_size - 1; //標誌pCur的最後一個有效鍵值位置
while (end >= 0)
{
// 比較當前位置上的鍵值與key的大小,找插入位置
// 若當前位置鍵值大於key
if (pCur->_keys[end] > key)
{
pCur->_keys[end + 1] = pCur->_keys[end]; // 向後移動當前位置上的鍵值
pCur->_pSons[end + 2] = pCur->_pSons[end + 1]; // 鍵值移動後,相應的更新孩子指向
}
else // 找到位置後退出迴圈
break;
end--;
}
// 插入key,並更新對應位置上pCur的孩子指向以及size
pCur->_keys[end + 1] = key;
pCur->_pSons[end + 2] = pSon;
pCur->_size++;
// 若孩子不為空,更新雙親為pCur
if (pSon)
pSon->_pParent = pCur;
}
private:
Node* _pRoot;
};
void Test()
{
BTree<int, 3> t;
t.Insert(10);
t.Insert(30);
t.Insert(20);
t.Insert(40);
t.Insert(50);
t.Insert(80);
t.Insert(70);
t.InOrder();
}