1. 程式人生 > >B樹B+樹B*樹原理及應用

B樹B+樹B*樹原理及應用

二叉查詢樹和平衡二叉樹都是典型的二叉查詢樹結構,查詢的時間複雜度O(log2N)與樹的深度相關,因此降低樹的高度自然對查詢效率有所幫助,為了降低樹的高度,可令每個節點儲存多個元素,並將平衡二叉查詢樹拓展為平衡多叉查詢樹,這時神奇的B樹就從石頭裡蹦出來了,B樹,B+樹與紅黑樹很大的不同點在於B樹的結點有多個子女。

1 .B樹定義

定義:一棵m 階的B樹,或者為空樹,或為滿足下列特性的m 叉樹:
⑴樹中每個結點至多有m 棵子樹;
⑵若根結點不是葉子結點,則至少有兩棵子樹;

⑶除根結點之外的所有非終端結點至少有[m/2] 棵子樹;
⑷所有的非終端結點中包含以下資訊資料:(n,A0,K1,A1,K2,…,Kn,An)

其中:Ki(i=1,2,…,n)為關鍵碼,且Ki<Ki+1,Ai為指向子樹根結點的指標(i=0,1,…,n),且指標Ai-1 所指子樹中所有結點的關鍵碼均小於Ki 且大於Ki-1.

關鍵字個數需要滿足 
⑸所有的葉子結點都出現在同一層次上,並且不帶資訊(可以看作是外部結點或查詢失敗的結點,實際上這些結點不存在,指向這些結點的指標為空)。

例圖如下:

查詢演算法如下:

typedef int KeyType ;  
#define m 5                 /*B 樹的階,暫設為5*/  
typedef struct Node{  
    int keynum;             /* 結點中關鍵碼的個數,即結點的大小*/  
    struct Node *parent;    /*指向雙親結點*/   
    KeyType key[m+1];       /*關鍵碼向量,0 號單元未用*/   
    struct Node *ptr[m+1];  /*子樹指標向量*/   
    Record *recptr[m+1];    /*記錄指標向量*/  
}NodeType;                  /*B 樹結點型別*/  
  
typedef struct{  
    NodeType *pt;           /*指向找到的結點*/  
    int i;                  /*在結點中的關鍵碼序號,結點序號區間[1…m]*/  
    int tag;                /* 1:查詢成功,0:查詢失敗*/  
}Result;                    /*B 樹的查詢結果型別*/  
  
Result SearchBTree(NodeType *t,KeyType kx)  
{   
    /*在m 階B 樹t 上查詢關鍵碼kx,反回(pt,i,tag)。若查詢成功,則特徵值tag=1,*/  
    /*指標pt 所指結點中第i 個關鍵碼等於kx;否則,特徵值tag=0,等於kx 的關鍵碼記錄*/  
    /*應插入在指標pt 所指結點中第i 個和第i+1 個關鍵碼之間*/  
    p=t;q=NULL;found=FALSE;i=0; /*初始化,p 指向待查結點,q 指向p 的雙親*/  
    while(p&&!found)  
    {   n=p->keynum;i=Search(p,kx);          /*在p-->key[1…keynum]中查詢*/  
        if(i>0&&p->key[i]= =kx) found=TRUE; /*找到*/  
        else {q=p;p=p->ptr[i];}  
    }  
    if(found) return (p,i,1);               /*查詢成功*/  
    else return (q,i,0);                    /*查詢不成功,反回kx 的插入位置資訊*/  
}  
int Search(BTree p,int k){//查詢該關鍵字應在的位置
int i = 1;
while (i <= p->keynum && k > p->key[i]){
 i++; 
}
return i++;
}
B樹的查詢過程是根據給定值查詢結點和在結點的關鍵字中進行查詢交叉進行。從根節點開始,重複以下過程:

若給定關鍵字等於結點中某個關鍵字Ki,則查詢成功;若給定關鍵字比結點中的K1小,則進入指標A0指向的下一層結點繼續查詢,若在兩個關鍵字Ki和Ki+1之間,則進入他們之間的指標Ai指向的下一層結點繼續查詢;若查詢到葉子結點,則說明給定值對應的資料記錄不存在,則查詢失敗。

   插入操作如下:通過查詢演算法找到關鍵字k的插入位置,若找到相同關鍵字則不需插入,否則在該插入點插入,若其關鍵字總數n未達到m,演算法結束;否則,需分裂結點。分裂操作:生成一新節點,從中間位置把結點分成兩部分。前半部分留在舊結點中,後半部分複製到新結點中,中間位置的關鍵字連同新節點的儲存位置插入到父節點中。如果插入後父節點的關鍵字個數也超過m-1,則要再分裂,向上類推。

   刪除操作如下:通過查詢演算法找出該節點位置,如果是最下層非終端結點,則進行以下判斷:

1.該節點關鍵字個數 >= m/2向上取整,則直接刪除即可。


2.如果關鍵字個數等於m/2-1,說明刪去該關鍵字後該節點不滿足B樹的定義,需要調整。調整過程為:如果其左右兄弟結點中關鍵字個數 >= m/2向上取整,則可將右(左)兄弟結點中最小(大)關鍵字上移至雙親結點。而將雙親結點中小(大)與該上移關鍵字的關鍵字下移至被刪關鍵字所在結點處。


3.如果雙親結點中沒有多餘的關鍵字,這時比較複雜,需要把刪除關鍵字的結點與其左(或右)兄弟結點以及雙親結點中分割二者的關鍵字合併成一個結點,即在刪除關鍵字後,該節點中剩餘的關鍵字加指標,加上雙親結點中的關鍵字Ki一起,合併到Ai(即雙親結點指向該刪除關鍵字結點的左(右)兄弟結點的指標)所指的兄弟結點中去。如果因此使雙親結點中關鍵字個數小於m/2向上取整-1,則對此雙親結點做同樣處理,以致可能直到對根節點做這樣的處理而使整個樹減少一層。


若該結點不是最下層非終端結點,且被刪關鍵字為該節點中第i個 關鍵字key[i],則可從指標ptr[i]所指的子樹中找出位於最下層非終端結點的最小關鍵字Y,替代key[i],然後在最底層非終端結點中移除Y,因此,把在非終端結點刪除關鍵字k的問題就程式設計了刪除最下層非終端結點中的關鍵字問題了。

刪除操作程式碼如下:

void DeleteBTree(BTree p,int i){

if(p->ptr[i-1] != null){//若不是最下層非終端結點 

Successor(p,i);//由後繼最下層非終端結點的最小關鍵字代替它

DeleteBTree(p,1);//變成刪除最下層非終端結點中的最小關鍵字

}else{

Remove(p,i);//從結點p中刪除key[i]

if(p->keynum < (m-1)/2)//刪除後關鍵字個數如果小於(m-1)/2

Restore(p,i);//調整B樹

}

}

上述演算法中的Successor、DeleteBTree、Restore具體演算法實現可由讀者自行完成。

B樹的應用場景:多用在記憶體放不下,需要放在外村的情況,應為B樹層數相對較少,能保證磁碟讀取的次數相對較少,主要應用在檔案系統上。

B+樹:B+樹是應檔案系統所需要而提出的一種B樹的變形。一棵m階B+樹和m階B樹的差別在於:

1.有n棵子樹的結點中含有n個關鍵字,每個關鍵字不儲存資料,只儲存索引。

2.所有的葉子結點中包含了全部關鍵字的資訊,及指向含這些關鍵字記錄的指標,且葉子結點本身依關鍵字的大小自小而大的順序連結

3.所有的非終端結點可以看成是索引部分,結點中僅含其子樹(根結點)中最大(或最小)關鍵字。

B+樹通常有兩個頭指標,一個指向根結點,一個指向關鍵字最小的葉子結點。因此可以對B+樹進行兩種方式的查詢:一種是從最小關鍵字起順序查詢,另一種是從根結點開始。

B+樹每個結點的關鍵字個數最多為m,而B樹則最多為m-1

在B+樹上查詢、插入、和刪除的過程基本上就與B樹類似。只是在查詢時,若非終端結點上的關鍵碼等於給定值,並不終止,而是繼續向下走到葉子結點,因此,不管找到與否,最後都會走到葉子結點

解釋一下插入時的情況,根據插入值的大小,逐步向下直到對應的葉子節點。如果葉子節點關鍵字個數小於2t,則直接插入值或者更新衛星資料;如果插入之前葉子節點已經滿了,則分裂該葉子節點成兩半,並把中間值提上到父節點的關鍵字中,如果這導致父節點滿了的話,則把該父節點分裂,如此遞歸向上。所以樹高是一層層的增加的,葉子節點永遠都在同一深度。

B+樹的刪除也僅在葉子結點進行,當葉子結點中的最大關鍵字被刪除時,其在非終端結點中的值可以作為一個“分界關鍵字”存在。若因刪除而使結點中關鍵字的個數少於m/2 (m/2結果取上界,如5/2結果為3)時,其和兄弟結點的合併過程亦和B-樹類似。

相信來看這篇文章的小夥伴都應該瞭解B+樹是資料庫索引的重要一員,下面就來分析一下該項內容。

1. 索引在資料庫中的作用 
        在資料庫系統的使用過程當中,資料的查詢是使用最頻繁的一種資料操作。
        最基本的查詢演算法當然是順序查詢(linear search),遍歷表然後逐行匹配行值是否等於待查詢的關鍵字,其時間複雜度為O(n)。但時間複雜度為O(n)的演算法規模小的表,負載輕的資料庫,也能有好的效能。  但是資料增大的時候,時間複雜度為O(n)的演算法顯然是糟糕的,效能就很快下降了。這時就需要一種結構來解決這個問題,提高查詢效率,這就是索引。
       索引是對資料庫表 中一個或多個列的值進行排序的結構。與在表 中搜索所有的行相比,索引用指標 指向儲存在表中指定列的資料值,然後根據指定的次序排列這些指標,有助於更快地獲取資訊。通常情 況下 ,只有當經常查詢索引列中的資料時 ,才需要在表上建立索引。索引將佔用磁碟空間,並且影響數 據更新的速度。但是在多數情況下 ,索引所帶來的資料檢索速度優勢大大超過它的不足之處。
2.為什麼說B+-tree比B 樹更適合實際應用中作業系統的檔案索引和資料庫索引?
1) B+-tree的磁碟讀寫代價更低
B+-tree的內部結點並沒有指向關鍵字具體資訊的指標。因此其內部結點相對B 樹更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那麼盤塊所能容納的關鍵字數量也越多。一次性讀入記憶體中的需要查詢的關鍵字也就越多。相對來說IO讀寫次數也就降低了。
    舉個例子,假設磁碟中的一個盤塊容納4K位元組,而一個關鍵字4位元組,一個關鍵字具體資訊指標4位元組。而B+ 樹關鍵字只需要4位元組。B+樹比B樹可以在塊中存放多一倍的關鍵字,當需要把內部結點讀入記憶體中的時候,B 樹就比B+ 樹多一次盤塊查詢時間(在磁碟中就是碟片旋轉的時間),B+比B快了一倍。

2) B+-tree的查詢效率更加穩定
由於非終結點並不是最終指向檔案內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查詢必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。
3)還有一個重要原因就是B樹在提高了磁碟IO效能的同時並沒有解決元素遍歷的效率低下的問題。正是為了解決這個問題,B+樹應運而生。B+樹只要遍歷葉子節點就可以實現整棵樹的遍歷。而且在資料庫中基於範圍的查詢是非常頻繁的,而B樹不支援這樣的操作(或者說效率太低)。

還有一種B*樹:

B*樹在B+樹的基礎上之上在非根結點和非葉子結點的結點增加了指向兄弟的指標(能跟左右兄弟聯絡是優點,缺點是消耗了相應資源),B*樹定義了非葉子結點至少有(2/3)*m個關鍵字(B樹是m/2)


B+樹的分裂:當一個結點滿時,分配一個新的結點,並將原結點中1/2的資料複製到新結點,最後在父結點中增加新結點的指標;B+樹的分裂隻影響原結點和父結點,而不會影響兄弟結點,所以它不需要指向兄弟的指標;
B*樹的分裂:當一個結點滿時,如果它的下一個兄弟結點未滿,那麼將一部分資料移到兄弟結點中,再在原結點插入關鍵字,最後修改父結點中兄弟結點的關鍵字(因為兄弟結點的關鍵字範圍改變了);如果兄弟也滿了,則在原結點與兄弟結點之間增加新結點,並各複製1/3的資料到新結點,最後在父結點增加新結點的指標;
所以,B*樹分配新結點的概率比B+樹要低,空間使用率更高;

該文章主要講了這三種樹的原理及應用,後面的文章會接著分析各樹在索引中的具體原理及不同儲存引擎中所用索引的不同,以及紅黑樹的知識。歡迎一起討論