為了cmu數據庫的Lab2作准備
1. B-Tree Family
→ B-Tree (1971)
→ B+Tree (1973)
→ B*Tree (1977?)
→ B link-Tree (1981)
2. B+ Tree的特性
- 完美平衡樹
- 根結點至少有兩個子女。
- 除了根結點以外的其他結點的關鍵字個數 $ \frac{m}{2} \le keys \le m-1 $。
- 內部結點有k個關鍵字就會有k+1個孩子
- 葉結點會用雙向鏈表連接起來。因為所有的value都保存在葉子結點。其他結點只保存索引,這樣可以支持順序索引和隨機索引

正常來講b+樹的所有元素都需要在葉子結點出現。

對於葉子結點的存儲有兩種形式
一種是存指針。一種存數據
- Record IDs: A pointer to the location of the tuple
- Tuple Data: The actual contents of the tuple is stored in the leaf node
3. B+ Tree 的插入
3.1 算法原理
-
若為空樹,創建一個葉子結點,然后將記錄插入其中,此時這個葉子結點也是根結點,插入操作結束。
-
針對葉子類型結點:根據key值找到葉子結點,向這個葉子結點插入記錄。插入后,若當前結點key的個數小於等於m-1,則插入結束。否則將這個葉子結點分裂成左右兩個葉子結點,左葉子結點包含前m/2個記錄,右結點包含剩下的記錄,將第m/2+1個記錄的key進位到父結點中(父結點一定是索引類型結點),進位到父結點的key左孩子指針向左結點,右孩子指針向右結點。將當前結點的指針指向父結點,然后執行第3步。
-
針對索引類型結點(內部結點):若當前結點key的個數小於等於m-1,則插入結束。否則,將這個索引類型結點分裂成兩個索引結點,左索引結點包含前\(\frac{(m-1)}{2}\)個key,右結點包含\(m- \frac{(m-1)}{2}\)個key,將第\(\frac{m}{2}\)個key進位到父結點中,進位到父結點的key左孩子指向左結點, 進位到父結點的key右孩子指向右結點。將當前結點的指針指向父結點,然后重復這一步。
cmu這里給了演示網站 https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html
假設我們要插入5, 8,10,15 ,16 , 20 ,19 。以m=3為例子
- 插入5,8直接根節點

-
插入10
由於此時根節點有3個結點>2(m-1)因此會分裂。而且這個時候是對葉子類型結點的處理。把前m/2=1個結點分給左葉子。右葉子包含剩下的結點。中間的 m/2+1第二個結點成為父節點。

-
插入15。15會插到根節點的右邊。然后就會出現和上面一樣的情況。因此我們繼續分裂

-
插入16
- 先插入到15的右邊,導致15所在的葉子結點分裂。會把15提到父節點。10成為左孩子,15 ,16為右孩子
- 遞歸向上檢查。會發現父節點有8,10,15三個結點也不符合要求。因此需要再次進行分裂。

-
插入20
20 會放到16的右邊。然后這個結點需要分裂。15成為左孩子,16 ,20 成為右孩子,16提為父結點就ok啦

-
插入19
- 會放到16左邊20右邊。因此這個結點會分裂,把19提到父節點
- 遞歸檢查的時候發現父節點也有三個結點這里也需要分裂

好了關於b+樹的插入模擬我們就到這里了。下面來寫一下代碼
3.2 代碼實現
一些在b+樹插入時代碼的時候思考的問題
-
split的時候需要找父結點怎么解決
一種是維護一個parent指針
-
查找插入結點
-
維護關鍵字有序如何做
因為用的數組存的關鍵字。所以就按照數組插入o(n)的復雜度
3.21 數據結構設計
- 用
*key表示關鍵字 - 用
**ptr表示結點 - 用
IS_LEAF來表示是否為頁子結點。
#include <iostream>
#include <queue>
using namespace std;
int MAX = 2;
// BP node
class Node {
bool IS_LEAF;
int *key, size;
Node** ptr;
Node* parent;
friend class BPTree;
public:
Node():key(new int[MAX+1]),ptr(new Node* [MAX+1]),parent(NULL){}
~Node();
};
// BP tree
class BPTree {
Node* root;
void insertInternal(int,Node*,Node*,Node*);
void split(int ,Node *,Node *);
int insertVal(int ,Node *);
public:
BPTree():root(NULL){}
void insert(int x);
void display();
};
3.22 普通插入
insertVal函數負責找到插入的位置並返回
int BPTree::insertVal(int x, Node *cursor) {
int i = 0;
while (x > cursor->key[i] && i < cursor->size) i++;
for (int j = cursor->size; j > i; j--) cursor->key[j] = cursor->key[j - 1];
cursor->key[i] = x;
cursor->size++;
return i;
}
insert函數負責進行插入這里分為幾種情況
- 根節點為空則創建一個根節點。
- 如果不為根節點則要找到插入位置(到葉結點才停止)同時記錄插入結點的父結點
- 如果插入結點滿足關鍵字個數<MAX( 就是M-1) 我們就可以直接插入。
- 否則需要
split
void BPTree::insert(int x) {
if (root == NULL) {
root = new Node;
root->key[0] = x;
root->IS_LEAF = true;
root->size = 1;
root->parent = NULL;
} else {
Node *cursor = root;
Node *parent;
while (cursor->IS_LEAF == false) {
parent = cursor;
for (int i = 0; i < cursor->size; i++) {
if (x < cursor->key[i]) {
cursor = cursor->ptr[i];
break;
}
if (i == cursor->size - 1) {
cursor = cursor->ptr[i + 1];
break;
}
}
}
if (cursor->size < MAX) {
insertVal(x,cursor);
cursor->parent = parent;
cursor->ptr[cursor->size] = cursor->ptr[cursor->size - 1];
cursor->ptr[cursor->size - 1] = NULL;
} else split(x, parent, cursor);
}
}
3.23 需要split的插入
這里要分兩種情況
- 葉子結點拆分之后。提上去的結點為根節點
- 否則需要調用
insertInternal函數
void BPTree::split(int x, Node * parent, Node *cursor) {
Node* LLeaf=new Node;
Node* RLeaf=new Node;
insertVal(x,cursor);
LLeaf->IS_LEAF=RLeaf->IS_LEAF=true;
LLeaf->size=(MAX+1)/2;
RLeaf->size=(MAX+1)-(MAX+1)/2;
for(int i=0;i<MAX+1;i++)LLeaf->ptr[i]=cursor->ptr[i];
LLeaf->ptr[LLeaf->size]= RLeaf;
RLeaf->ptr[RLeaf->size]= LLeaf->ptr[MAX];
LLeaf->ptr[MAX] = NULL;
for (int i = 0;i < LLeaf->size; i++) {
LLeaf->key[i]= cursor->key[i];
}
for (int i = 0,j=LLeaf->size;i < RLeaf->size; i++,j++) {
RLeaf->key[i]= cursor->key[j];
}
if(cursor==root){
Node* newRoot=new Node;
newRoot->key[0] = RLeaf->key[0];
newRoot->ptr[0] = LLeaf;
newRoot->ptr[1] = RLeaf;
newRoot->IS_LEAF = false;
newRoot->size = 1;
root = newRoot;
LLeaf->parent=RLeaf->parent=newRoot;
}
else {insertInternal(RLeaf->key[0],parent,LLeaf,RLeaf);}
}
3.24 insertInternal插入的實現
基本思路都是差不多的。就是需要注意遞歸調用
- 如果由於拆分之后提上去的結點不會再產生拆分則直接插入
- 再拆如果提到根節點則創建新的根節點
- 否則就繼續調用
insertInternal
void BPTree::insertInternal(int x,Node* cursor,Node* LLeaf,Node* RRLeaf)
{
if (cursor->size < MAX) {
auto i=insertVal(x,cursor);
for (int j = cursor->size;j > i + 1; j--) {
cursor->ptr[j]= cursor->ptr[j - 1];
}
cursor->ptr[i]=LLeaf;
cursor->ptr[i + 1] = RRLeaf;
}
else {
Node* newLchild = new Node;
Node* newRchild = new Node;
Node* virtualPtr[MAX + 2];
for (int i = 0; i < MAX + 1; i++) {
virtualPtr[i] = cursor->ptr[i];
}
int i=insertVal(x,cursor);
for (int j = MAX + 2;j > i + 1; j--) {
virtualPtr[j]= virtualPtr[j - 1];
}
virtualPtr[i]=LLeaf;
virtualPtr[i + 1] = RRLeaf;
newLchild->IS_LEAF=newRchild->IS_LEAF = false;
//這里和葉子結點上有區別的
newLchild->size= (MAX + 1) / 2;
newRchild->size= MAX - (MAX + 1) /2;
for (int i = 0;i < newLchild->size;i++) {
newLchild->key[i]= cursor->key[i];
}
for (int i = 0, j = newLchild->size+1;i < newRchild->size;i++, j++) {
newRchild->key[i]= cursor->key[j];
}
for (int i = 0;i < LLeaf->size + 1;i++) {
newLchild->ptr[i]= virtualPtr[i];
}
for (int i = 0, j = LLeaf->size + 1;i < RRLeaf->size + 1;i++, j++) {
newRchild->ptr[i]= virtualPtr[j];
}
if (cursor == root) {
Node* newRoot = new Node;
newRoot->key[0]= cursor->key[newLchild->size];
newRoot->ptr[0] = newLchild;
newRoot->ptr[1] = newRchild;
newRoot->IS_LEAF = false;
newRoot->size = 1;
root = newRoot;
newLchild->parent=newRchild->parent=newRoot;
}
else {
insertInternal(cursor->key[newLchild->size],cursor->parent,newLchild,newRchild);
}
}
}
3.25 展示函數的實現
這里用了一個簡單的層次遍歷。來實現展示函數
void BPTree::display() {
queue<Node*>q;
q.push(root);
while(!q.empty()){
int size_t=q.size();
while(size_t--){
auto t=q.front();
for(int i=0;i<t->size+1;i++){
if(!t->IS_LEAF){
q.push(t->ptr[i]);
}
}
for(int i=0;i<t->size;i++){
cout<<t->key[i]<<",";
}
cout<<" ";
q.pop();
}
cout<<endl;
}
}
3.26 結果
假設我們要插入5, 8,10,15 ,16 , 20 ,19。以m=3(MAX=2)為例子
得到的結果如下

程序運行結果如下
,表示在一個結點內
三個空格表示不同的結點

可以發現代碼是正確的。完整的代碼見下面的GitHub地址
