B-樹 動機與結構
Ps.我們遵循從感性到理性的認知順序來逐步探索B-樹的奧秘,之前經常說的value這裏用key(關鍵碼)指代,因為可能存的是字符串,說是value就不合適了。
(多圖預警!!!建議在WI-FI下觀看)
雖然迄今為止我們所看到的查找樹皆為二叉樹,但還有另一種常用的查找樹與此相異,名為B- 樹,又叫B樹。下面是B-樹的基本信息
在物理上,B-樹的每一個節點都可能包含多個分支,然而後面會看到,從邏輯上講,它依然等效於此前所介紹的二叉查找樹,因此我們依然把它歸入搜索樹的範疇。那已經有了之前那麽多的種類,設計和實現B-樹的動機何在呢?B樹最初也是最主要的功能,在於彌合不同存儲級別之間在訪問速度上的巨大差異——也就是實現高效的IO。先讓我們穿越到37年前,那時Bill Gates的一句話曾經被很多人當作笑柄。
因為他在那時曾經斷言640KB也就是dos的基本內存容量,已經足以滿足任何實際應用的需要了。雖然這話有點武斷,但在學習完B樹以後,我們一定會認為這句話實際上是千真萬確的真理,因為我們有各種各樣的玄學優化手段2333。
從某種意義上講,我們在計算過程中能夠使用的內存是在日益的變小,而不是如我們直覺一樣,變得越來越大。這聽起來似乎是個悖論,但原因在於需要處理的信息量級越來越大:系統存儲容量的增長速度<<應用問題規模的增長速度。不妨來看這樣一組統計數字
而我們人類所擁有的數字化信息的總量,在過去的半個多世紀中增長速度是驚人的,比如截至2010年總量以及達到Zettabyte——1後面要接21個0。我們知道中國的人口大致是十多億,也就是$10^{9}$左右,分攤下去每個人都需要一個TB規模的硬盤(如果硬盤裏女朋友比較多這空間還不夠呢233),註意,這裏我們說的還只是硬盤,是外部存儲。而如果考慮內存,那這方面的壓力就更大了。
不妨來進一步看一些數字,對不同年代典型的數據庫規模和內存規模做個對比:
短短20年,內存規模躍升1000倍,但是數據規模則躍升了百萬、千萬倍。內存容量的壓力其實更大了,這個壓力提高了至少100倍。而如今典型的數據集已經大多以TB作為度量單位了,無論是生物分子學、醫學、物理學,還是核能、氣象等領域。隨便舉幾個例子
總而言之,盡管隨著技術的發展,內存的絕對容量的確是在增加,但是相對於實際應用的需求而言,內存的容量實際上是在越來越小。那為什麽不把內存做大一點啊?原因在於,經費你給撥麽?我們必須在容量與訪問速度之間做取舍,組成原理裏講過,存儲器的容量越大速度就越慢,反過來,為了使得訪問速度更快,就不得不在容量上做必要的犧牲。面對這個內在矛盾,我們還是可以有所作為的,運用矛盾分析法,我們想到了一種絕妙的方法——Cache!為此我們需要對不同層次存儲器的性能做進一步的了解:
這包括兩個事實
- 容量和類型不同的存儲器,在訪問速度上的差異是極其懸殊的
就以磁盤、內存這兩級存儲為例,他們在訪問速度上的差異究竟有多大呢?就傳統的旋轉式磁盤而言,訪問速度大致是ms級。而典型的內存呢,大致是在ns級,不要小看了m和n之間的差異,以一秒為基準——前者是$10^{-3}$,而後者是$10^{-9}$。故二者的差異大致是在$10^{5}$至$10^{6}$。即使保守的估計也是5個數量級。就是一秒之於一天的差距。如果將內存的一次訪問比作是1s,那麽響應的一次外存操作則是1 day。這個差距很令人絕望了……《醒世恒言》裏形象的說過:山中方七日,世上已千年。
因此在設計與實現算法的時候,為了避免一次外存訪問,我們寧可訪問內存十次、百次甚至千次、萬次也在所不惜。這也是為什麽通常存儲系統都是按層次分級組織的。隨著層次的深入,存儲器的容量越來越大,但是反過來,訪問的速度也越來越低。這樣一種分級的結構之所以能夠高效的運轉,在於其中采用的一種策略:將最常用的數據盡可能放在更高的層次,因為盡管它的存儲容量有限但速度最高,而不常用的數據會自適應的轉移到更大,但是速度更慢的級別中去。
- 從磁盤讀寫1B,與讀寫1KB幾乎一樣快
典型的存儲系統的確大多是采用批量式的方式來支持讀或者寫操作的。具體來說,無論我們是需要從內存向外存輸出數據,還是需要從外存向內存讀入數據,涉及的數據都是以頁面為單位進行核算和組織的。比如在C的stdio.h中有這樣一段代碼:
其中setvbuf接口就允許設置頁面緩沖區的大小,緩沖的工作模式等。因此在涉及頻繁而大量數據訪問的算法中,就要充分利用這個特性,也就是說我們要逐漸習慣批量式的訪問。要麽一次性大量讀寫,要麽就什麽也不做。就邊際成本而言,這樣的組織和訪問方式才能夠達到盡可能的優化。那麽我們的主角B-樹在其間又能起到一個什麽樣的作用呢?
那我們來探究一下B-樹的內部了。
B樹也是用來存放一組具有關鍵碼的詞條的,它的特點也非常的鮮明,首先每一個節點未必只有兩個分叉,可以擁有更多的分叉。其次,所有底層節點的深度都是完全一致的,從這個意義上講它是一種理想平衡的搜索樹。最後,也是最重要的一個整體特征:相對於常規的二叉查找樹,B樹會顯得更寬、更矮,而且也是可以動態變化的。B樹的設計者將其定義為一種平衡的多路(multi-way)搜索樹,與之前的二路搜索樹在本質上是等價的,因為每一個內部(internal)節點都可以認為是由若幹個二路節點經過適當的合並以後得到的
舉個例子,看這個
不看方框,這就是一顆BST,看了方框,把父子兩代合並為一個節點,這棵樹就變成了這樣:
2代合並後,每個節點都將擁有3個關鍵碼,以及4個分支。推而廣之,3代合並後,每個節點裏含有7個關鍵碼以及8路分支。一般而言, 如果每d代都進行一次合並,那麽之後的每個節點都將擁有$2^{d}$路分支,以及相應再減少1個單位的關鍵碼。那既然這種多路的搜索樹與二路搜索樹並沒有本質的區別,那還發明個鬼啊,難道這是灌水論文?
非也非也,問題在於我們通常都是按多個層次來分級組織的存儲系統,如果使用B樹可以針對不同層次間的通信,大大降低IO訪問的次數,從而極大的提高計算效率。那難道之前很熟悉的AVL樹在這方面還不夠麽?
我們來做個小學算術,有一個由$10^{9}$,1G個記錄的數據集。如果將它們組織為一棵AVL樹,高度大致為30層。也就是說在最壞的情況下,單次查找需要深入30層,而每一層我們都需要執行一次IO操作,而每一次只能讀一個值,這就很得不償失了。那B樹呢,B樹中合並後節點同時包含多個而不是單個關鍵碼,因此在B樹中每下降一層,都能以內部節點為單位讀入一組而不是單個關鍵碼,從而將外存批量訪問的特點轉化為實在的優點。一個內部節點的規模取決於數據緩沖頁面的大小,通常的情況下都是幾個KB。比如若將內部節點的規模取做256,$2^{8}$。那同樣存1G個記錄的B樹高度不會超過4,這就意味著即便在最壞的情況下,單次查找所需的IO也同樣不超過4次,這是一個很大的提高了。或許有些人會有疑問:這不都是常數級別麽?就漸近意義而言是這樣,但是當這個常數的每一個單位都相當於105至106時,就必須計較一下了,因為我們的內存和時間都是有限的。就像1秒和1天都可以視作是常數,但是對於有限的人生來說,卻有本質的區別,4年讀一個學士大家能接受,如果換成30年,就沒人這麽幹了。
有了以上的感性認識,我們從理性上洞察一下B-樹為何物。每一顆B-樹都有自己的階次,這是它的固有屬性。M階B-樹是一顆具有以下結構特征的樹:
- 樹根處限制:0個兒子 or 兒子數量在2和M之間
- 除樹根外,所有非葉節點的兒子數量在$\left\lceil \frac{M}{2} \right\rceil$和M之間
- 所有的樹葉都在相同深度,外部節點也在相同深度
- 每個內部節點有不超過M-1個關鍵碼
換句話講,M這個指標規定了B-樹內部節點和分支的上下界:M階B樹每個節點最多有M條分支,除根之外,其他節點至少有$\left\lceil \frac{M}{2} \right\rceil$條分支,這棵樹又叫($\left\lceil \frac{M}{2} \right\rceil$,M)-樹。節點最多存M-1個值,其他的節點至少有實際分支數-1個值(by wikipedia)。對於4階B樹而言,也可以稱之為(2,4)樹,有趣的是,(2,4)樹在B樹中具有非常獨特的作用和地位,後面我們將會看到(2,4)樹與紅黑樹有不解的淵源。這篇寫B-樹分析,下篇寫具體實現,再下一篇就寫紅黑樹。
紅色的部分是真實存在的葉子結點,在很多文獻中可以和“外部節點”互稱,但在B-樹中這是兩個完全不同的概念。另外B-樹的高度是把外部節點也計入在內的,與通常的BST不同。還需要說明的是,關於B-樹的表示問題,因為他特殊的性質導致很多情況下要預留出很多指針位,具體來說就是,如果需要完整的將一棵B樹畫出來,那就要為每一個關鍵碼的左右後代畫出2個指針,如下
但是紙沒那麽大,所以要緊湊一點來表示,我們把這些指針簡化為質點,變成這樣:
而外部節點都在同一層,沒有差異,就把它忽略掉,我們只關註不同點。可以這樣表示:
或者這樣:
如此一來就能節省篇幅地表達巨量的數據和引用了。但是畫歸畫,實際上心裏還是要明白,裏面存在大量的外部節點和引用。接下來的一個問題自然就是:“你說的道理我懂,可是為什麽鴿子那麽大?” 該如何實現B-樹呢?又該如何完成配套的一系列維護操作呢?各位下篇見,明天6點左右發。
B-樹 動機與結構