B+樹詳解+代碼實現(插入篇)

爲了cmu數據庫的Lab2做準備html

1. B-Tree Family

→ B-Tree (1971)node

→ B+Tree (1973)ios

→ B*Tree (1977?)c++

→ B link-Tree (1981)git

2. B+ Tree的特性

  1. 完美平衡樹
  2. 根結點至少有兩個子女。
  3. 除了根結點之外的其餘結點的關鍵字個數 $ \frac{m}{2} \le keys \le m-1 $。
  4. 內部結點有k個關鍵字就會有k+1個孩子
  5. 葉結點會用雙向鏈表鏈接起來。由於全部的value都保存在葉子結點。其餘結點只保存索引,這樣能夠支持順序索引和隨機索引

正常來說b+樹的全部元素都須要在葉子結點出現。
github

對於葉子結點的存儲有兩種形式算法

一種是存指針。一種存數據數據庫

  1. Record IDs: A pointer to the location of the tuple
  2. 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爲例子

  1. 插入5,8直接根節點

  1. 插入10

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

  1. 插入15。15會插到根節點的右邊。而後就會出現和上面同樣的狀況。所以咱們繼續分裂

  2. 插入16

    1. 先插入到15的右邊,致使15所在的葉子結點分裂。會把15提到父節點。10成爲左孩子,15 ,16爲右孩子
    2. 遞歸向上檢查。會發現父節點有8,10,15三個結點也不符合要求。所以須要再次進行分裂。

  1. 插入20

    20 會放到16的右邊。而後這個結點須要分裂。15成爲左孩子,16 ,20 成爲右孩子,16提爲父結點就ok啦

  1. 插入19

    1. 會放到16左邊20右邊。所以這個結點會分裂,把19提到父節點
    2. 遞歸檢查的時候發現父節點也有三個結點這裏也須要分裂

好了關於b+樹的插入模擬咱們就到這裏了。下面來寫一下代碼

3.2 代碼實現

一些在b+樹插入時代碼的時候思考的問題

  • split的時候須要找父結點怎麼解決

    一種是維護一個parent指針

  • 查找插入結點

  • 維護關鍵字有序如何作

    由於用的數組存的關鍵字。因此就按照數組插入o(n)的複雜度

3.21 數據結構設計
  1. *key表示關鍵字
  2. **ptr表示結點
  3. 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函數負責進行插入這裏分爲幾種狀況

  1. 根節點爲空則建立一個根節點。
  2. 若是不爲根節點則要找到插入位置(到葉結點才中止)同時記錄插入結點的父結點
  3. 若是插入結點知足關鍵字個數<MAX( 就是M-1) 咱們就能夠直接插入。
  4. 不然須要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的插入

這裏要分兩種狀況

  1. 葉子結點拆分以後。提上去的結點爲根節點
  2. 不然須要調用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插入的實現

基本思路都是差很少的。就是須要注意遞歸調用

  1. 若是因爲拆分以後提上去的結點不會再產生拆分則直接插入
  2. 再拆若是提到根節點則建立新的根節點
  3. 不然就繼續調用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地址

https://github.com/JayL-zxl/BPlusTree

相關文章
相關標籤/搜索