1. 程式人生 > >經典資料結構 [ B樹,B+樹 ]+B樹的應用

經典資料結構 [ B樹,B+樹 ]+B樹的應用

關於B樹的原理和實現方法,我也是研究了好久才看明白的,沒明白之前感覺一臉懵逼,看懂後才發現原來也很簡單。所以同學們要是發現很難看懂的情況下,不要煩躁著急,可以先冷靜冷靜的思考一下,然後多看幾篇文章,我也是看了好幾篇的文章才看懂的,要是大家看完之後還是不大懂的話,可以再文章最後聯絡我,加油!

B 樹是為了磁碟或其它儲存裝置而設計的一種多叉(下面你會看到,相對於二叉,B樹每個內結點有多個分支,即多叉)平衡查詢樹。

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

  1. 樹中每個結點最多含有m個孩子(m>=2);
  2. 除根結點和葉子結點外,其它每個結點至少有[ceil(m / 2)]個孩子(其中ceil(x)是一個取上限的函式);
  3. 若根結點不是葉子結點,則至少有2個孩子(特殊情況:沒有孩子的根結點,即根結點為葉子結點,整棵樹只有一個根節點);
  4. 所有葉子結點都出現在同一層,葉子結點不包含任何關鍵字資訊(可以看做是外部接點或查詢失敗的接點,實際上這些結點不存在,指向這些結點的指標都為null);
  5. 每個非終端結點中包含有n個關鍵字資訊: (n,P0,K1,P1,K2,P2,......,Kn,Pn)。其中:
           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。


來模擬下查詢檔案29的過程:

    (1) 根據根結點指標找到檔案目錄的根磁碟塊1,將其中的資訊匯入記憶體。【磁碟IO操作1次】

    (2) 此時記憶體中有兩個檔名17,35和三個儲存其他磁碟頁面地址的資料。根據演算法我們發現17<29<35,因此我們找到指標p2。

    (3) 根據p2指標,我們定位到磁碟塊3,並將其中的資訊匯入記憶體。【磁碟IO操作2次】

    (4) 此時記憶體中有兩個檔名26,30和三個儲存其他磁碟頁面地址的資料。根據演算法我們發現26<29<30,因此我們找到指標p2。

    (5) 根據p2指標,我們定位到磁碟塊8,並將其中的資訊匯入記憶體。【磁碟IO操作3次】

    (6) 此時記憶體中有兩個檔名28,29。根據演算法我們查詢到檔案29,並定位了該檔案記憶體的磁碟地址。

插入操作

生成從空樹開始,逐個插入關鍵字。但是由於B_樹節點關鍵字必須大於等於[ceil(m/2)-1],所以每次插入一個關鍵字不是在樹中新增一個葉子結點,而是首先在最底層的某個非終端節點中新增一個“關鍵字”,該結點的關鍵字不超過m-1,則插入完成;否則要產生結點的“分裂”,將一半數量的關鍵字元素分裂到新的其相鄰右結點中,中間關鍵字元素上移到父結點中。

1、咱們通過一個例項來逐步講解下。插入以下字元字母到一棵空的樹中(非根結點關鍵字數小了(小於2個)就合併,大了(超過4個)就分裂):C N G A H E K Q M F W L T Z D P R X Y S首先,結點空間足夠,4個字母插入相同的結點中,如下圖:

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

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

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

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

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

7、插入D時,導致最左邊的葉子結點被分裂,D恰好也是中間元素,上移到父節點中,然後字母P,R,X,Y陸續插入不需要任何分裂操作(別忘了,樹中至多5個孩子)。

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

刪除操作

首先查詢B樹中需刪除的元素,如果該元素在B樹中存在,則將該元素在其結點中進行刪除,如果刪除該元素後,首先判斷該元素是否有左右孩子結點,如果有,則上移孩子結點中的某相近元素到父節點中,然後移動之後情況;如果沒有,直接刪除後,移動之後的情況

刪除元素,移動相應元素之後,如果某結點中元素數目(即關鍵字數)小於ceil(m/2)-1,則需要看其某相鄰兄弟結點是否豐滿(結點中元素個數大於ceil(m/2)-1)(還記得第一節中關於B樹的第5個特性中的c點麼?: c)除根結點之外的結點(包括葉子結點)的關鍵字的個數n必須滿足: (ceil(m / 2)-1)<= n <= m-1。m表示最多含有m個孩子,n表示關鍵字數。在本小節中舉的一顆B樹的示例中,關鍵字數n滿足:2<=n<=4),如果豐滿,則向父節點借一個元素來滿足條件;如果其相鄰兄弟都剛脫貧,即借了之後其結點數目小於ceil(m/2)-1,則該結點與其相鄰的某一兄弟結點進行合併”成一個結點,以此來滿足條件。那咱們通過下面例項來詳細瞭解吧。

以上述插入操作構造的一棵5階B樹(樹中最多含有m(m=5)個孩子,因此關鍵字數最小為ceil(m / 2)-1=2。還是這句話,關鍵字數小了(小於2個)就合併,大了(超過4個)就分裂)為例,依次刪除H,T,R,E

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

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

3、下一步刪除RR在葉子結點中,但是該結點中元素數目為2,刪除導致只有1個元素,已經小於最小元素數目ceil(5/2)-1=2,而由前面我們已經知道:如果其某個相鄰兄弟結點中比較豐滿(元素個數大於ceil(5/2)-1=2),則可以向父結點借一個元素,然後將最豐滿的相鄰兄弟結點中上移最後或最前一個元素到父節點中(有沒有看到紅黑樹中左旋操作的影子?),在這個例項中,右相鄰兄弟結點中比較豐滿(3個元素大於2),所以先向父節點借一個元素W下移到該葉子結點中,代替原來S的位置,S前移;然後X在相鄰右兄弟結點中上移到父結點中,最後在相鄰右兄弟結點中刪除X,後面元素前移。

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

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

為了進一步詳細討論刪除的情況,再舉另外一個例項:

這裡是一棵不同的5B樹,那咱們試著刪除C

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

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

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

從以上操作可看出:除根結點之外的結點(包括葉子結點)的關鍵字的個數n滿足:(ceil(m / 2)-1)<= n <= m-1,即2<=n<=4。這也佐證了咱們之前的觀點。刪除操作完。

在B_樹中關鍵字分佈在整個B_樹,並且在上層結點中出現過的關鍵字不再出現在最底層的結點中。順序鏈中所有的關鍵字不能連線在一起。

一顆m階的B+樹和m階的B_樹的差異在於:

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

2.所有的葉子結點中包含了全部關鍵字的資訊,及指向含有這些關鍵字記錄的指標,且葉子結點本身依關鍵字的大小自小而大的順序連結。(而B樹的葉子節點並沒有包括全部需要查詢的資訊)

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

 

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

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

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

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

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

分析

對B樹和B+樹的分析和對前面講解的2-3樹的分析類似,

對於一顆節點為N度為M的子樹,查詢和插入需要logM-1N ~ logM/2N次比較。這個很好證明,對於度為M的B樹,每一個節點的子節點個數為M/2 到 M-1之間,所以樹的高度在logM-1N至logM/2N之間。

這種效率是很高的,對於N=62*1000000000個節點,如果度為1024,則logM/2N <=4,即在620億個元素中,如果這棵樹的度為1024,則只需要小於4次即可定位到該節點,然後再採用二分查詢即可找到要找的值。

應用

B樹和B+廣泛應用於檔案儲存系統以及資料庫系統中,在講解應用之前,我們看一下常見的儲存結構:

File System

我們計算機的主存基本都是隨機訪問儲存器(Random-Access Memory,RAM),他分為兩類:靜態隨機訪問儲存器(SRAM)和動態隨機訪問儲存器(DRAM)。SRAM比DRAM快,但是也貴的多,一般作為CPU的快取記憶體,DRAM通常作為記憶體。這類儲存器他們的結構和儲存原理比較複雜,基本是使用電訊號來儲存資訊的,不存在機器操作,所以訪問速度非常快,具體的訪問原理可以檢視CSAPP,另外,他們是易失的,即如果斷電,儲存DRAM和SRAM儲存的資訊就會丟失。

我們使用的更多的是使用磁碟,磁碟能夠儲存大量的資料,從GB一直到TB級,但是 他的讀取速度比較慢,因為涉及到機器操作,讀取速度為毫秒級,從DRAM讀速度比從磁碟度快10萬倍,從SRAM讀速度比從磁碟讀快100萬倍。下面來看下磁碟的結構:

Disk geometry

如上圖,磁碟由碟片構成,每個碟片有兩面,又稱為盤面(Surface),這些盤面覆蓋有磁性材料。碟片中央有一個可以旋轉的主軸(spindle),他使得碟片以固定的旋轉速率旋轉,通常是5400轉每分鐘(Revolution Per Minute,RPM)或者是7200RPM。磁碟包含一個多多個這樣的碟片並封裝在一個密封的容器內。上圖左,展示了一個典型的磁碟表面結構。每個表面是由一組成為磁軌(track)的同心圓組成的,每個磁軌被劃分為了一組扇區(sector).每個扇區包含相等數量的資料位,通常是(512)子節。扇區之間由一些間隔(gap)隔開,不儲存資料。

以上是磁碟的物理結構,現在來看下磁碟的讀寫操作:

Disk dynamic

如上圖,磁碟用讀/寫頭來讀寫儲存在磁性表面的位,而讀寫頭連線到一個傳動臂的一端。通過沿著半徑軸前後移動傳動臂,驅動器可以將讀寫頭定位到任何磁軌上,這稱之為尋道操作。一旦定位到磁軌後,碟片轉動,磁軌上的每個位經過磁頭時,讀寫磁頭就可以感知到位的值,也可以修改值。對磁碟的訪問時間分為 尋道時間旋轉時間,以及傳送時間

由於儲存介質的特性,磁碟本身存取就比主存慢很多,再加上機械運動耗費,因此為了提高效率,要儘量減少磁碟I/O,減少讀寫操作。為了達到這個目的,磁碟往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個位元組,磁碟也會從這個位置開始,順序向後讀取一定長度的資料放入記憶體。這樣做的理論依據是電腦科學中著名的區域性性原理:

當一個數據被用到時,其附近的資料也通常會馬上被使用。

程式執行期間所需要的資料通常比較集中。

由於磁碟順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對於具有區域性性的程式來說,預讀可以提高I/O效率。

預讀的長度一般為頁(page)的整倍數。頁是計算機管理儲存器的邏輯塊,硬體及作業系統往往將主存和磁碟儲存區分割為連續的大小相等的塊,每個儲存塊稱為一頁(在許多作業系統中,頁得大小通常為4k),主存和磁碟以頁為單位交換資料。當程式要讀取的資料不在主存中時,會觸發一個缺頁異常,此時系統會向磁碟發出讀盤訊號,磁碟會找到資料的起始位置並向後連續讀取一頁或幾頁載入記憶體中,然後異常返回,程式繼續執行。

檔案系統及資料庫系統的設計者利用了磁碟預讀原理,將一個節點的大小設為等於一個頁,這樣每個節點只需要一次I/O就可以完全載入。為了達到這個目的,在實際實現B-Tree還需要使用如下技巧:

每次新建一個節點的同時,直接申請一個頁的空間( 512或者1024),這樣就保證一個節點物理上也儲存在一個頁裡,加之計算機儲存分配都是按頁對齊的,就實現了一個node只需一次I/O。如,將B樹的度M設定為1024,這樣在前面的例子中,600億個元素中只需要小於4次查詢即可定位到某一儲存位置。

同時在B+樹中,內節點只儲存導航用到的key,並不儲存具體值,這樣內節點個數較少,能夠全部讀取到主存中,外接點儲存key及值,並且順序排列,具有良好的空間區域性性。所以B及B+樹比較適合與檔案系統的資料結構。下面是一顆B樹,用來進行內容儲存。

build a large B tree

另外B/B+樹也經常用做資料庫的索引,這方面推薦您直接看張洋的MySQL索引背後的資料結構及演算法原理 這篇文章,這篇文章對MySQL中的如何使用B+樹進行索引有比較詳細的介紹,推薦閱讀。

總結

在前面兩篇文章介紹了平衡查詢樹中的2-3樹紅黑樹之後,本文介紹了檔案系統和資料庫系統中常用的B/B+ 樹,他通過對每個節點儲存個數的擴充套件,使得對連續的資料能夠進行較快的定位和訪問,能夠有效減少查詢時間,提高儲存的空間區域性性從而減少IO操作。他廣泛用於檔案系統及資料庫中,如:

  • Windows:HPFS檔案系統
  • Mac:HFS,HFS+檔案系統
  • Linux:ResiserFS,XFS,Ext3FS,JFS檔案系統
  • 資料庫:ORACLE,MYSQL,SQLSERVER等中

對文章有什麼疑問或者想要看更多文章可以加我訂閱號,歡迎大家的踩踩~