B-tree/B+tree/B*tree

1.前言:

動態查找樹主要有:二叉查找樹(Binary Search Tree),平衡二叉查找樹(Balanced Binary Search Tree),紅黑樹 (Red-Black Tree ),B-tree/B+-tree/ B*-tree (B~Tree)。前三者是典型的二叉查找樹結構,其查找的時間複雜度O(log2N)與樹的深度相關,那麼降低樹的深度自然對查找效率是有所提高的;還有一個實際問題:就是大規模數據存儲中,實現索引查詢這樣一個實際背景下,樹節點存儲的元素數量是有限的(如果元素數量非常多的話,查找就退化成節點內部的線性查找了),這樣導致二叉查找樹結構由於樹的深度過大而造成磁盤I/O讀寫過於頻繁,進而導致查詢效率低下(爲什麼會出現這種情況,待會在外部存儲器-磁盤中有所解釋),那麼如何減少樹的深度(當然是不能減少查詢的數據量),一個基本的想法就是:採用多叉樹結構(由於樹節點元素數量是有限的,自然該節點的子樹數量也就是有限的)。

這樣我們就提出了一個新的查找樹結構——多路查找樹。根據平衡二叉樹的啓發,自然就想到平衡多路查找樹結構,也就是這篇文章所要闡述的主題B~tree(B樹結構),B-tree這棵神奇的樹是在Rudolf BayerEdward M. McCreight(1970)寫的一篇論文《Organization and Maintenance of Large Ordered Indices》中首次提出。具體介紹可以參考wikipedia中的介紹:http://en.wikipedia.org/wiki/B-tree,其中還闡述了B-tree名字來源以及相關的開源地址。

在開始介紹B~tree之前,先了解下相關的硬件知識,才能很好的瞭解爲什麼需要B~tree這種外存數據結構。

2.外存儲器磁盤

計算機存儲設備一般分爲兩種內存儲器(main memory)和外存儲器(external memory)內存存取速度快,但容量小,價格昂貴,而且不能長期保存數據(在不通電情況下數據會消失)

外存儲器—磁盤是一種直接存取的存儲設備(DASD)。它是以存取時間變化不大爲特徵的。可以直接存取任何字符組,且容量大、速度較其它外存設備更快。

2.1磁盤的構造

磁盤時一個扁平的圓盤(與電唱機的唱片類似)。盤面上有許多稱爲磁道的圓圈,數據就記錄在這些磁道上。磁盤可以是單片的,也可以是由若干盤片組成的盤組,每一盤片上有兩個面。如下圖6片盤組爲例,除去最頂端和最底端的外側面不存儲數據之外,一共有10個面可以用來保存信息。

                            

 

當磁盤驅動器執行讀/寫功能時。盤片裝在一個主軸上,並繞主軸高速旋轉,當磁道在讀/寫頭(又叫磁頭下通過時,就可以進行數據的讀 / 寫了。

一般磁盤分爲固定頭盤(磁頭固定)和活動頭盤。固定頭盤的每一個磁道上都有獨立的磁頭,它是固定不動的,專門負責這一磁道上數據的讀/寫。

活動頭盤 (如上圖)的磁頭是可移動的。每一個盤面上只有一個磁頭(磁頭是雙向的,因此正反盤面都能讀寫)。它可以從該面的一個磁道移動到另一個磁道。所有磁頭都裝在同一個動臂上,因此不同盤面上的所有磁頭都是同時移動的(行動整齊劃一)。當盤片繞主軸旋轉的時候,磁頭與旋轉的盤片形成一個圓柱體。各個盤面上半徑相同的磁道組成了一個圓柱面,我們稱爲柱面。因此,柱面的個數也就是盤面上的磁道數。

2.2磁盤的讀/寫原理和效率

磁盤上數據必須用一個三維地址唯一標示:柱面號、盤面號、塊號(磁道上的盤塊)

/寫磁盤上某一指定數據需要下面3個步驟:

(1)  首先移動臂根據柱面號使磁頭移動到所需要的柱面上,這一過程被稱爲定位或查找

(2)  如上圖6盤組示意圖中,所有磁頭都定位到了10個盤面的10條磁道上(磁頭都是雙向的)。這時根據盤面號來確定指定盤面上的磁道。

(3) 盤面確定以後,盤片開始旋轉,將指定塊號的磁道段移動至磁頭下。

經過上面三個步驟,指定數據的存儲位置就被找到。這時就可以開始讀/寫操作了。

訪問某一具體信息,由3部分時間組成:

● 查找時間(seek time) Ts: 完成上述步驟(1)所需要的時間。這部分時間代價最高,最大可達到0.1s左右。

● 等待時間(latency time) Tl: 完成上述步驟(3)所需要的時間。由於盤片繞主軸旋轉速度很快,一般爲7200/(電腦硬盤的性能指標之一家用的普通硬盤的轉速一般有5400rpm(筆記本)7200rpm幾種)因此一般旋轉一圈大約0.0083s

● 傳輸時間(transmission time) Tt: 數據通過系統總線傳送到內存的時間,一般傳輸一個字節(byte)大概0.02us=2*10^(-8)s

磁盤讀取數據是以盤塊(block)爲基本單位的位於同一盤塊中的所有數據都能被一次性全部讀取出來。而磁盤IO代價主要花費在查找時間Ts上。因此我們應該儘量將相關信息存放在同一盤塊,同一磁道中。或者至少放在同一柱面或相鄰柱面上,以求在讀/寫信息時儘量減少磁頭來回移動的次數,避免過多的查找時間Ts

所以,在大規模數據存儲方面,大量數據存儲在外存磁盤中,而在外存磁盤中讀取/寫入塊(block)中某數據時,首先需要定位到磁盤中的某塊,如何有效地查找磁盤中的數據,需要一種合理高效的外存數據結構,就是下面所要重點闡述的B-tree結構,以及相關的變種結構:B+-tree結構和B*-tree結構。

3.B-tree

 

B-tree又叫平衡多路查找樹。一棵m階的B-tree (m叉樹)的特性如下:

(其中ceil(x)是一個取上限的函數)

1)  樹中每個結點至多有m個孩子;

2)  除根結點和葉子結點外,其它每個結點至少有有ceil(m / 2)個孩子;

3)  若根結點不是葉子結點,則至少有2個孩子(特殊情況:沒有孩子的根結點,即根結點爲葉子結點,整棵樹只有一個根節點);

4)  所有葉子結點都出現在同一層,葉子結點不包含任何關鍵字信息(可以看做是外部結點或查詢失敗的結點,實際上這些結點不存在,指向這些結點的指針都爲null)

5)  每個非終端結點中包含有n個關鍵字信息: (nP0K1P1K2P2......KnPn)。其中:

             a)   Ki (i=1...n)爲關鍵字,且關鍵字按順序排序K(i-1)< Ki

             b)   Pi爲指向子樹根的接點,且指針P(i-1)指向子樹種所有結點的關鍵字均小於Ki,但都大於K(i-1)

      c)   關鍵字的個數n必須滿足: ceil(m / 2)-1 <= n <= m-1

B-tree中的每個結點根據實際情況可以包含大量的關鍵字信息和分支(當然是不能超過磁盤塊的大小,根據磁盤驅動(disk drives)的不同,一般塊的大小在1k~4k左右);這樣樹的深度降低了,這就意味着查找一個元素只要很少結點從外存磁盤中讀入內存,很快訪問到要查找的數據。

 

 

爲了簡單,這裏用少量數據構造一棵3叉樹的形式。上面的圖中比如根結點,其中17表示一個磁盤文件的文件名;小紅方塊表示這個17文件的內容在硬盤中的存儲位置;p1表示指向17左子樹的指針。

其結構可以簡單定義爲:

typedef struct {

    /*文件數*/

    int  file_num;

    /*文件名(key)*/

    char * file_name[max_file_num];

    /*指向子節點的指針*/

     BTNode * BTptr[max_file_num+1];

     /*文件在硬盤中的存儲位置*/

     FILE_HARD_ADDR offset[max_file_num];

}BTNode;

假如每個盤塊可以正好存放一個B-tree的結點(正好存放2個文件名)。那麼一個BTNode結點就代表一個盤塊,而子樹指針就是存放另外一個盤塊的地址。

模擬查找文件29的過程:

 (1) 根據根結點指針找到文件目錄的根磁盤塊1,將其中的信息導入內存。【磁盤IO操作1次】

 (2) 此時內存中有兩個文件名1735和三個存儲其他磁盤頁面地址的數據。根據算法我們發現17<29<35,因此我們找到指針p2

 (3) 根據p2指針,我們定位到磁盤塊3,並將其中的信息導入內存。【磁盤IO操作2次】

 (4) 此時內存中有兩個文件名2630和三個存儲其他磁盤頁面地址的數據。根據算法我們發現26<29<30,因此我們找到指針p2

 (5) 根據p2指針,我們定位到磁盤塊8,並將其中的信息導入內存。【磁盤IO操作3次】

 (6) 此時內存中有兩個文件名2829。根據算法我們查找到文件29,並定位了該文件內存的磁盤地址。

分析上面的過程,發現需要3次磁盤IO操作和3次內存查找操作。關於內存中的文件名查找,由於是一個有序表結構,可以利用折半查找提高效率。至於3次磁盤IO操作時影響整個B-tree查找效率的決定因素。

當然,如果我們使用平衡二叉樹的磁盤存儲結構來進行查找,磁盤IO操作最少4次,最多5次。而且文件越多,B-tree比平衡二叉樹所用的磁盤IO操作次數將越少,效率也越高。

上面僅僅介紹了對於B-tree這種結構的查找過程,還有樹節點的插入與刪除過程,以及相關的算法和代碼的實現,將在以後的深入學習中給出相應的實例

上面簡單介紹了利用B-tree這種結構如何訪問外存磁盤中的數據的情況,下面咱們通過另外一個實例來對這棵B-tree的插入(insert),刪除(delete)基本操作進行詳細的介紹:

下面以一棵5階B-tree實例進行講解(如下圖所示):

其滿足上述條件:除根結點和葉子結點外,其它每個結點至少有ceil(5/2)=3個孩子(至少2個關鍵字);當然最多5個孩子(最多4個關鍵字)。下圖中關鍵字爲大寫字母,順序爲字母升序。

結點定義如下:

typedef struct{

   int Count;         // 當前節點中關鍵元素數目

   ItemType Key[4];   // 存儲關鍵字元素的數組

   long Branch[5];    // 僞指針數組,(記錄數目)方便判斷合併和分裂的情況

} NodeType;

插入(insert)操作插入一個元素時,首先在B-tree中是否存在,如果不存在,即在葉子結點處結束,然後在葉子結點中插入該新的元素,注意:如果葉子結點空間足夠,這裏需要向右移動該葉子結點中大於新插入關鍵字的元素,如果空間滿了以致沒有足夠的空間去添加新的元素,則將該結點進行“分裂”,將一半數量的關鍵字元素分裂到新的其相鄰右結點中,中間關鍵字元素上移到父結點中(當然,如果父結點空間滿了,也同樣需要“分裂”操作),而且當結點中關鍵元素向右移動了,相關的指針也需要向右移。如果在根結點插入新元素,空間滿了,則進行分裂操作,這樣原來的根結點中的中間關鍵字元素向上移動到新的根結點中,因此導致樹的高度增加一層。

咱們通過一個實例來逐步講解下。插入以下字符字母到空的5階B-tree中:C N G A H E K Q M F W L T Z D P R X Y S5序意味着一個結點最多有5個孩子和4個關鍵字,除根結點外其他結點至少有2個關鍵字,首先,結點空間足夠,4個字母插入相同的結點中,如下圖:

 

當咱們試着插入H時,結點發現空間不夠,以致將其分裂成2個結點,移動中間元素G上移到新的根結點中,在實現過程中,咱們把AC留在當前結點中,而HN放置新的其右鄰居結點中。如下圖:

 

當咱們插入E,K,Q時,不需要任何分裂操作

 

插入M需要一次分裂,注意M恰好是中間關鍵字元素,以致向上移到父節點中

 

插入F,W,L,T不需要任何分裂操作

 

插入Z時,最右的葉子結點空間滿了,需要進行分裂操作,中間元素T上移到父節點中,注意通過上移中間元素,樹最終還是保持平衡,分裂結果的結點存在2個關鍵字元素。

 

插入D時,導致最左邊的葉子結點被分裂,D恰好也是中間元素,上移到父節點中,然後字母P,R,X,Y陸續插入不需要任何分裂操作。

 

最後,當插入S時,含有N,P,Q,R的結點需要分裂,把中間元素Q上移到父節點中,但是情況來了,父節點中空間已經滿了,所以也要進行分裂,將父節點中的中間元素M上移到新形成的根結點中,注意以前在父節點中的第三個指針在修改後包括DG節點中。這樣具體插入操作的完成,下面介紹刪除操作,刪除操作相對於插入操作要考慮的情況多點。

 

刪除(delete)操作:首先查找B-tree中需刪除的元素,如果該元素在B-tree中存在,則將該元素在其結點中進行刪除,如果刪除該元素後,首先判斷該元素是否有左右孩子結點,如果有,則上移孩子結點中的某相近元素到父節點中,然後是移動之後的情況;如果沒有,直接刪除後,移動之後的情況.。

刪除元素,移動相應元素之後,如果某結點中元素數目小於ceil(m/2)-1,則需要看其某相鄰兄弟結點是否豐滿(結點中元素個數大於ceil(m/2)-1),如果豐滿,則向父節點借一個元素來滿足條件;如果其相鄰兄弟都剛脫貧,即借了之後其結點數目小於ceil(m/2)-1,則該結點與其相鄰的某一兄弟結點進行合併成一個結點,以此來滿足條件。那咱們通過下面實例來詳細瞭解吧。

以上述插入操作構造的一棵5階B-tree爲例,依次刪除H,T,R,E

首先刪除元素H,當然首先查找HH在一個葉子結點中,且該葉子結點元素數目3大於最小元素數目ceil(m/2)-1=2,則操作很簡單,咱們只需要移動K至原來H的位置,移動LK的位置(也就是結點中刪除元素後面的元素向前移動)

 

下一步,刪除T,因爲T沒有在葉子結點中,而是在中間結點中找到,咱們發現他的繼承者W(字母升序的下個元素),將W上移到T的位置,然後將原包含W的孩子結點中的W進行刪除,這裏恰好刪除W後,該孩子結點中元素個數大於2,無需進行合併操作。

 

下一步刪除RR在葉子結點中,但是該結點中元素數目爲2,刪除導致只有1個元素,已經小於最小元素數目ceil(5/2)-1=2,如果其某個相鄰兄弟結點中比較豐滿(元素個數大於ceil(5/2)-1=2),則可以向父結點借一個元素,然後將最豐滿的相鄰兄弟結點中上移最後或最前一個元素到父節點中,在這個實例中,右相鄰兄弟結點中比較豐滿(3個元素大於2),所以先向父節點借一個元素W下移到該葉子結點中,代替原來S的位置,S前移;然後X在相鄰右兄弟結點中上移到父結點中,最後在相鄰右兄弟結點中刪除X,後面元素前移。

 

 

最後一步刪除E刪除後會導致很多問題,因爲E所在的結點數目剛好達標,剛好滿足最小元素個數(ceil(5/2)-1=2,而相鄰的兄弟結點也是同樣的情況,刪除一個元素都不能滿足條件,所以需要該節點與某相鄰兄弟結點進行合併操作;首先移動父結點中的元素(該元素在兩個需要合併的兩個結點元素之間)下移到其子結點中,然後將這兩個結點進行合併成一個結點。所以在該實例中,咱們首先將父節點中的元素D下移到已經刪除E而只有F的結點中,然後將含有DF的結點和含有A,C的相鄰兄弟結點進行合併成一個結點。

 

也許你認爲這樣刪除操作已經結束了,其實不然,在看看上圖,對於這種特殊情況,你立即會發現父節點只包含一個元素G,沒達標,這是不能夠接受的。如果這個問題結點的相鄰兄弟比較豐滿,則可以向父結點借一個元素。假設這時右兄弟結點(含有Q,X)有一個以上的元素(Q右邊還有元素),然後咱們將M下移到元素很少的子結點中,將Q上移到M的位置,這時,Q的左子樹將變成M的右子樹,也就是含有NP結點被依附在M的右指針上。所以在這個實例中,咱們沒有辦法去借一個元素,只能與兄弟結點進行合併成一個結點,而根結點中的唯一元素M下移到子結點,這樣,樹的高度減少一層。

 

爲了進一步詳細討論刪除的情況。再舉另外一個實例:

這裏是一棵不同的5階B-tree,那咱們試着刪除C

 

於是將刪除元素C的右子結點中的D元素上移到C的位置,但是出現上移元素後,只有一個元素的結點的情況。

 

又因爲含有E的結點,其相鄰兄弟結點纔剛脫貧(最少元素個數爲2),不可能向父節點借元素,所以只能進行合併操作,於是這裏將含有A,B的左兄弟結點和含有E的結點進行合併成一個結點。

 

這樣又出現只含有一個元素F結點的情況,這時,其相鄰的兄弟結點是豐滿的(元素個數爲3>最小元素個數2),這樣就可以想父結點借元素了,把父結點中的J下移到該結點中,相應的如果結點中J後有元素則前移,然後相鄰兄弟結點中的第一個元素(或者最後一個元素)上移到父節點中,後面的元素(或者前面的元素)前移(或者後移);注意含有KL的結點以前依附在M的左邊,現在變爲依附在J的右邊。這樣每個結點都滿足B-tree結構性質。

 

如果想了解相關代碼,見最後參考。

 

4.B+-tree

B+-tree:是應文件系統所需而產生的一種B-tree的變形樹。

一棵m階的B+-treem階的B-tree的差異在於:

       1.n棵子樹的結點中含有n個關鍵字; (B-treen棵子樹有n-1個關鍵字)

       2.所有的葉子結點中包含了全部關鍵字的信息,及指向含有這些關鍵字記錄的指針,且葉子結點本身依關鍵字的大小自小而大的順序鏈接。 (B-tree的葉子節點並沒有包括全部需要查找的信息)

       3.所有的非終端結點可以看成是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵字。 (B-tree的非終節點也包含需要查找的有效信息)

 

 

a)      爲什麼說B+B-tree更適合實際應用中操作系統的文件索引和數據庫索引?

1) B+-tree的磁盤讀寫代價更低

B+-tree的內部結點並沒有指向關鍵字具體信息的指針。因此其內部結點相對B-tree更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那麼盤塊所能容納的關鍵字數量也越多。一次性讀入內存中的需要查找的關鍵字也就越多。相對來說IO讀寫次數也就降低了。

     舉個例子,假設磁盤中的一個盤塊容納16bytes,而一個關鍵字2bytes,一個關鍵字具體信息指針2bytes。一棵9B-tree(一個結點最多8個關鍵字)的內部結點需要2個盤快。而B+-tree內部結點只需要1個盤快。當需要把內部結點讀入內存中的時候,B-tree就比B+-tree多一次盤塊查找時間(在磁盤中就是盤片旋轉的時間)

2) B+-tree的查詢效率更加穩定

由於非終結點並不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。

b)      B+-tree的應用: VSAM(虛擬存儲存取法)文件(來源論文the ubiquitous Btree 作者:D COMER - 1979 )

 

 

關於B+-tree的詳細介紹將在以後的學習中給出實例,待寫。。。

5.B*-tree

B*-treeB+-tree的變體,在B+-tree的非根和非葉子結點再增加指向兄弟的指針;B*-tree定義了非葉子結點關鍵字個數至少爲(2/3)*M,即塊的最低使用率爲2/3(代替B+樹的1/2)。給出了一個簡單實例,如下圖所示:

 

B+-tree的分裂:當一個結點滿時,分配一個新的結點,並將原結點中1/2的數據複製到新結點,最後在父結點中增加新結點的指針;B+-tree的分裂隻影響原結點和父結點,而不會影響兄弟結點,所以它不需要指向兄弟的指針。

B*-tree的分裂:當一個結點滿時,如果它的下一個兄弟結點未滿,那麼將一部分數據移到兄弟結點中,再在原結點插入關鍵字,最後修改父結點中兄弟結點的關鍵字(因爲兄弟結點的關鍵字範圍改變了);如果兄弟也滿了,則在原結點與兄弟結點之間增加新結點,並各複製1/3的數據到新結點,最後在父結點增加新結點的指針。

所以,B*-tree分配新結點的概率比B+-tree要低,空間使用率更高;

 

6.總結

      B-treeB+-treeB*-tree總結如下: 

       B-tree:有序數組+平衡多叉樹;

       B+-tree:有序數組鏈表+平衡多叉樹;

       B*-tree:一棵豐滿的B+-tree

 

        在大規模數據存儲的文件系統中,B~tree系列數據結構,起着很重要的作用,對於存儲不同的數據,節點相關的信息也是有所不同,這裏根據自己的理解,畫的一個查找以職工號爲關鍵字,職工號爲38的記錄的簡單示意圖。(這裏假設每個物理塊容納3個索引,磁盤的I/O操作的基本單位是塊(block),磁盤訪問很費時,採用B+-tree有效的減少了訪問磁盤的次數。)

對於像MySQLDB2Oracle等數據庫中的索引結構有待深入的瞭解才行,不過網上可以找到很多B-tree相關的開源代碼可以用來研究。

 



自己的理解:

b+樹插入過程:

①、找到關鍵字應該在的葉子節點,如果插入沒有破壞b+樹的規則,則把數據和關鍵字插入到該葉子節點中。

②、如果插入破壞了規則,則把該葉子節點分裂,然後把中間的關鍵字(注意:只有關鍵字,沒有數據)移到父節點中。如果該關鍵字插入父節點沒有破壞規則,則結束。

③、如果插入到父節點破壞規則,則父節點分裂。然後繼續第二步。

注:b+樹的與b樹插入的不同之處在於,b樹分裂的時候,會把關鍵字移除分裂後的節點,但b+不會移除。



參考文獻(google下可以找到相關論文下載)以及相關網址:

1.     Organization and Maintenance of Large Ordered Indices

2.     the ubiquitous B tree

3.     http://en.wikipedia.org/wiki/Btree (給出了國外一些開源地址)

4.     http://cis.stvincent.edu/html/tutorials/swd/btree/btree.html(include C++ source code)

5.     http://slady.net/java/bt/view.php(如果瞭解了B-tree結構,該地址可以在線對該結構進行查找(search),插入(insert),刪除(delete)操作。)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章