數據結構相對來說比較枯燥, 我盡量用最易懂的話,來把B樹講清楚。
學過數據結構的人都接觸過一個概念----二叉樹。簡單來說,就是每個父節點最多有兩個子節點。
為了在二叉樹上更快的進行元素的查找,人們通過不斷的改進,從而設計出一種高效搜索的樹----平衡二叉查找樹,也就是這個樣子:
平衡二叉查找樹的特性由於不是本文的重點,這里就不再展開了。值得一提的是平衡二叉查找樹已經基本滿足了我們平常的軟件開發需求了。但是對於一些需要持久化數據並且支持查詢的業務來說,平衡二叉查找樹存在一個明顯的問題:
如果數據已經持久化到硬盤里邊,而我們又想要查詢數據的話,我們是需要先把數據先加載到內存里邊,再進行比較。
想一想你是不是沒法直接判斷硬盤里邊包含某一段關鍵字?
如果想要判斷,必須要先把數據讀到內存里邊才可以。如果數據量小的話,這種加載硬盤數據的性能損耗基本可以忽略掉,倒是也沒什么影響。可是如果數據量大的話,你總不能一次把全部數據加載到內存中再計算。即使你能等,內存也支撐不住。所以我們的辦法就是分段查找,一段一段的取到內存里邊進行比較,可是這樣無論是取多大,怎么比較,又是一個問題。而且更要命的是,倘若過於頻繁的一段段從硬盤中取數據的話,浪費在讀取數據的性能實在讓人可惜。
基於種種原因,於是有人對平衡二叉查找樹提出了改良:
1970年Rudolf Bayer,Edward M. McCreight 首次在論文中提到了一種新型的樹,並且稱之為B樹,意味balance tree 平衡樹,也稱之為 B-樹(千萬不可稱之為B減樹哦),B_樹等。
其實原理很簡單,節點不再是二叉查找樹那樣的只保存一個關鍵字,而是保存了多個關鍵字。這些關鍵字按照順序排好。然后還是按照左邊當前節點中的關鍵字都小,右邊比當前節點中的數據都大的形式,進行擴展。簡單來看,就是這個樣子了:
接着為了增加子節點繼續擴展的能力,允許一個節點可以多叉,但是依賴的原則還是基本不變的:每一個節點(更准確的說法是關鍵字)的左分叉要比當前節點的數字小,右分叉要比當前節點數字大。
所以我們基本可以理解為B樹是通過平衡二叉樹演化而來:
到這里,我們基本就算是搞懂B樹大概是長什么樣子了。
試想一下,如果是這個樣子的話,我們的程序就可以先把數據按照節點為單位,一次讀取若干個關鍵字到內存中。(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )。
然后在內存中進行比較,接着確定好目標所在的下一個分叉,然后獲取下一個分叉節點的數據。大概是下邊這個樣子:
但是出於更嚴格要求,B樹的定義要復雜的多:
首先我們要明白一個詞:階 degree(注意這個概念非常重要)
這個詞用來描述一個節點能包含的最大關鍵字的孩子的個數,也就是說節點最多有多少個分叉,而節點能裝的關鍵字的個數,就是分叉樹-1.
注意這個階是不隨着節點關鍵字的增加和減少來改變的,而是最初定義的一個屬性。節點增加關鍵字和減少關鍵字都不會改變這個樹最初定義的階的。
接下來圍繞這個階我們設定一些規則,保證B樹增加和減少關鍵字后,整個樹仍然是高效可用的。
(1) 樹中每個節點最多有m個孩子
直白的說:每個節點最多有m個分叉
(2) 除去根節點這葉子節點外,其它節點至少有m/2個孩子
(3) 根節點至少有2個孩子
直白的說:如果是樹中間的節點(非根非葉子),那么每個節點至少都有一半的分叉有孩子,如果是根節點那么就最少有2個孩子
(4) 所有葉節點在同一層,B樹的葉節點可以看成是一種外部節點,不包含任何信息
直白的說:所有的葉節點都和高度最高的葉節點呢,畫在一個水平線上,這些葉子節點呢,是用來記錄外部信息的。可以用空指針表示,代表查找失敗到達的位置。
(5) 有k個關鍵字(注意節點中的關鍵字要排好順序)的非葉節點恰好有k+1個孩子。
直白的說:1、節點中的關鍵字排好順序,這樣方便我們查找
2、有k個關鍵字就要有k+1個分叉(孩子)
如下圖,就是一個多層的B樹了,但是要注意,這棵B樹畫的並不標准,有兩點被模糊掉了:
(1)最下層的節點並非葉子,葉子節點是基於這一層節點作為父節點的子節點,在圖中葉子節點沒有被畫出來。(參考第四條)
(2)每個節點中,除了有若干關鍵字,還包含這些關鍵字所對應的data信息,data可以直接是內容,或者是指向內容的指針。(注意第二點非常重要)
接下來基於這棵B樹,我們舉個例子,來查找17這個數字:
第一步:內存加載根節點13,我們比較發現17>13,找13的右側分叉節點(15,20)
第二步:內存加載節點(15,20),我們比較15,發現 17>15,再比較20,發現17<20,於是取出15的右側分叉節點(16,17)
第三步:內存加載節點(16,17),我們比較16,發現17>16,再比較17,發現17=17,發現命中,取出17所對應的數據。
我們再舉個例子,來查找18這個數字:
前兩步都相同
第三步:內存加載節點(16,17),我們比較16,發現18>16,再比較17,發現18>17,於是我們要找17右側的分叉,但是此時右側的葉子節點為空(17的右側分叉對應葉子節點,葉子節點為空),所以我們斷定,18不存在。
注意無論是否存在,我們最多都只用了3次內存加載,就完成了比較查找。
這里要特別提下,為啥我們只看重內存加載的速度,而忽略比較次數的耗時呢?(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )這是因為我們在分析性能問題時,需要着重性能的瓶頸來分析。磁盤的讀取和內存的訪問接近有5個數量級的差異(單位大概是10毫秒與50微秒的差距)。因此我們在這里比較性能時,就是要看進行了多少次磁盤的讀取(磁盤的IO),並且主要以減少磁盤IO的手段來提升性能。
當然為了優化比較次數,我們還可以采用二分查找的方式,來判斷節點中是否包含某個關鍵字,進一步加快速度。
接下來影響提升整個IO次數的瓶頸就出現在,一個節點到底能存儲多少個關鍵字,如果關鍵字存儲的越多,我們一次加載到內存中的數據也就越多。同時也要注意,這個關鍵字的個數不能設置成無限大,因為內存不足以支撐一次加載太多的數據。
基於以上種種,我們可以發現,B樹是基於傳統硬盤與內存之間的IO差距,而專門設計出來的數據結構,他天然就適用於文件系統。
而對於B樹的升級版B+樹(B plus tree),我會在接下來的文章中專門講講,它又有什么不一樣的地方。