天天看點

B , B+ ,B*樹

從B 樹、B+ 樹、B* 樹談到R 樹 

作者:July、weedge、Frankie。程式設計藝術室出品。

說明:本文從B樹開始談起,然後論述B+樹、B*樹,最後談到R 樹。其中B樹、B+樹及B*樹部分由weedge完成,R 樹部分由Frankie完成,全文最終由July統稿修訂完成。

第一節、B樹、B+樹、B*樹

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(B-tree樹即B樹)這棵神奇的樹是在​​Rudolf Bayer​​​, ​​Edward 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磁盤的構造

磁盤是一個扁平的圓盤(與電唱機的唱片類似)。盤面上有許多稱為磁道的圓圈,資料就記錄在這些磁道上。磁盤可以是單片的,也可以是由若幹盤片組成的盤組,每一盤片上有兩個面。如下圖11.3中所示的6片盤組為例,除去最頂端和最底端的外側面不存儲資料之外,一共有10個面可以用來儲存資訊。

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

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

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

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

磁盤上資料必須用一個三維位址唯一标示:柱面号、盤面号、塊号(磁道上的盤塊)。

讀/寫磁盤上某一指定資料需要下面3個步驟:

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

(2)  如上圖11.3中所示的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- 樹 

 具體講解之前,有一點,再次強調下:B-樹,即為B樹。因為B樹的原英文名稱為B-tree,而國内很多人喜歡把B-tree譯作B-樹,其實,這是個非常不好的直譯,很容易讓人産生誤解。如人們可能會以為B-樹是一種樹,而B樹又是一種一種樹。而事實上是,B-tree就是指的B樹。特此說明。

我們知道,B 樹是為了磁盤或其它儲存設備而設計的一種多叉(下面你會看到,相對于二叉,B樹每個内結點有多個分支,即多叉)平衡查找樹。與本blog之前介紹的紅黑樹很相似,但在降低磁盤I/0操作方面要更好一些。許多​​資料庫​​系統都一般使用B樹或者B樹的各種變形結構,如下文即将要介紹的B+樹,B*樹來存儲資訊。

 B樹與紅黑樹最大的不同在于,B樹的結點可以有許多子女,從幾個到幾千個。那為什麼又說B樹與紅黑樹很相似呢?因為與紅黑樹一樣,一棵含n個結點的B樹的高度也為O(lgn),但可能比一棵紅黑樹的高度小許多,應為它的分支因子比較大。是以,B樹可以在O(logn)時間内,實作各種如插入(insert),删除(delete)等動态集合操作。

如下圖所示,即是一棵B樹,一棵關鍵字為英語中輔音字母的B樹,現在要從樹種查找字母R(包含n[x]個關鍵字的内結點x,x有n[x]+1]個子女(也就是說,一個内結點x若含有n[x]個關鍵字,那麼x将含有n[x]+1個子女)。所有的葉結點都處于相同的深度,帶陰影的結點為查找字母R時要檢查的結點):

相信,從上圖你能輕易的看到,一個内結點x若含有n[x]個關鍵字,那麼x将含有n[x]+1個子女。如含有2個關鍵字D H的内結點有3個子女,而含有3個關鍵字Q T X的内結點有4個子女。

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。

    針對上面第5點,再闡述下:B樹中每一個結點能包含的關鍵字(如之前上面的D H和Q T X)數有一個上界和下界。這兩個界可以用一個稱作B樹的最小度數(​​算法​​導論中文版上譯作度數)t(t>=2)表示。

  • 每個非根的結點必須至少含有t-1個關鍵字。每個非根的内結點至少有t個子女。如果樹是非空的,則根結點至少包含一個關鍵字;
  • 每個結點可包含之多2t-1個關鍵字。是以一個内結點至多可有2t個子女。如果一個結點恰好有2t-1個關鍵字,我們就說這個結點是滿的(而稍後介紹的B*樹作為B樹的一種常用變形,B*樹中要求每個内結點至少為2/3滿,而不是像這裡的B樹所要求的至少半滿);
  • 當關鍵字數t=2(t=2的意思是,tmin=2,t可以>=2)時的B樹是最簡單的(有很多人會是以誤認為B樹就是二叉查找樹,但二叉查找樹就是二叉查找樹,B樹就是B樹,B樹的真正最準确的定義為:一棵含有t(t>=2)個關鍵字的平衡多路查找樹)。每個内結點可能是以而含有2個、3個或4個子女,亦即一棵2-3-4樹,然而在實際中,通常采用大得多的t值。

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

為了簡單,這裡用少量資料構造一棵3叉樹的形式,實際應用中的B樹結點中關鍵字很多的。上面的圖中比如根結點,其中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 樹的結點(正好存放2個檔案名)。那麼一個BTNode結點就代表一個盤塊,而子樹指針就是存放另外一個盤塊 的位址。

下面,咱們來模拟下查找檔案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,并定位了該檔案記憶體的磁盤位址。

分析上面的過程,發現需要3次磁盤IO操作和3次記憶體查找操作。關于記憶體中的檔案名查找,由于是一個有序表結構,可以利用折半查找提高效率。至于3次磁盤IO操作時影響整個B樹查找效率的決定因素。

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

4.B+-tree

B+-tree:是應檔案系統所需而産生的一種B-tree的變形樹。

一棵m階的B+樹和m階的B樹的差異在于:

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

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

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

a)     為什麼說B+-tree比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+-tree的應用: VSAM(虛拟存儲存取法)檔案(來源論文 the ubiquitous Btree 作者:D COMER - 1979 )

5.B*-tree

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

B+樹的分裂:當一個結點滿時,配置設定一個新的結點,并将原結點中1/2的資料複制到新結點,最後在父結點中增加新結點的指針;B+樹的分裂隻影響原結點和父結點,而不會影響兄弟結點,是以它不需要指向兄弟的指針。

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

是以,B*樹配置設定新結點的機率比B+樹要低,空間使用率更高;

6、B樹的插入、删除操作

上面第3小節簡單介紹了利用B樹這種結構如何通路外存磁盤中的資料的情況,下面咱們通過另外一個執行個體來對這棵B樹的插入(insert),删除(delete)基本操作進行詳細的介紹。

  1. 樹中每個結點含有最多含有m個孩子,即m滿足:ceil(m/2)<=m<=m。
  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(葉子結點也必須滿足此條關于關鍵字數的性質,根結點除外)。

ok,下面咱們以一棵5階(m=5,即除根結點和葉子結點之外的内結點最多5個孩子,最少3個孩子)B樹執行個體進行講解(如下圖所示):

備注:關鍵字數(2-4個)針對--非根結點(包括葉子結點在内),孩子數(3-5個)--針對根結點和葉子結點之外的内結點。當然,根結點是必須至少有2個孩子的,不然就成直線型搜尋樹了。

下圖中關鍵字為大寫字母,順序為字母升序。

結點定義如下:

typedef struct{

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

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

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

} NodeType;

6.1、插入(insert)操作

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

1、咱們通過一個執行個體來逐漸講解下。插入以下字元字母到一棵空的B 樹中(非根結點關鍵字數小了(小于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上移到新的根結點中,在實作過程中,咱們把A和C留在目前結點中,而H和N放置新的其右鄰居結點中。如下圖:

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上移到新形成的根結點中,注意以前在父節點中的第三個指針在修改後包括D和G節點中。這樣具體插入操作的完成,下面介紹删除操作,删除操作相對于插入操作要考慮的情況多點。

6.2、删除(delete)操作

首先查找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,當然首先查找H,H在一個葉子結點中,且該葉子結點元素數目3大于最小元素數目ceil(m/2)-1=2,則操作很簡單,咱們隻需要移動K至原來H的位置,移動L至K的位置(也就是結點中删除元素後面的元素向前移動)

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

3、下一步删除R,R在葉子結點中,但是該結點中元素數目為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的結點中,然後将含有D和F的結點和含有A,C的相鄰兄弟結點進行合并成一個結點。

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

為了進一步詳細讨論删除的情況,再舉另外一個執行個體:

這裡是一棵不同的5序B樹,那咱們試着删除C

于是将删除元素C的右子結點中的D元素上移到C的位置,但是出現上移元素後,隻有一個元素的結點的情況。

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

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

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

7.總結

通過以上介紹,大緻将B樹,B+樹,B*樹總結如下:

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

B+樹:有序數組連結清單+平衡多叉樹;

B*樹:一棵豐滿的B+樹。

    在大規模資料存儲的檔案系統中,B~tree系列資料結構,起着很重要的作用,對于存儲不同的資料,節點相關的資訊也是有所不同,這裡根據自己的了解,畫的一個查找以職工号為關鍵字,職工号為38的記錄的簡單示意圖。(這裡假設每個實體塊容納3個索引,磁盤的I/O操作的基本機關是塊(block),磁盤通路很費時,采用B+樹有效的減少了通路磁盤的次數。)

對于像​​MySQL​​,DB2,​​Oracle​​等資料庫中的索引結構得有較深入的了解才行,建議去找一些B 樹相關的開源代碼研究。

走進搜尋引擎的作者梁斌老師針對B樹、B+樹給出了他的意見(為了真實性,特引用其原話,未作任何改動): “B+樹還有一個最大的好處,友善掃庫,B樹必須用中序周遊的方法按序掃庫,而B+樹直接從葉子結點挨個掃一遍就完了,B+樹支援range-query非常友善,而B樹不支援。這是資料庫選用B+樹的最主要原因。

    比如要查 5-10之間的,B+樹一把到5這個标記,再一把到10,然後串起來就行了,B樹就非常麻煩。B樹的好處,就是成功查詢特别有利,因為樹的高度總體要比B+樹矮。不成功的情況下,B樹也比B+樹稍稍占一點點便宜。

    B樹比如你的例子中查,17的話,一把就得到結果了,

有很多基于頻率的搜尋是選用B樹,越頻繁query的結點越往根上走,前提是需要對query做統計,而且要對key做一些變化。

    另外B樹也好B+樹也好,根或者上面幾層因為被反複query,是以這幾塊基本都在記憶體中,不會出現讀磁盤IO,一般已啟動的時候,就會主動換入記憶體。”非常感謝。

    Bucket Li:"mysql 底層存儲是用B+樹實作的,知道為什麼麼。記憶體中B+樹是沒有優勢的,但是一到磁盤,B+樹的威力就出來了"。

第二節、R樹:處理空間存儲問題

相信經過上面第一節的介紹,你已經對B樹或者B+樹有所了解。這種樹可以非常好的處理一維空間存儲的問題。B樹是一棵平衡樹,它是把一維直線分為若幹段線段,當我們查找滿足某個要求的點的時候,隻要去查找它所屬的線段即可。依我看來,這種思想其實就是先找一個大的空間,再逐漸縮小所要查找的空間,最終在一個自己設定的最小不可分空間内找出滿足要求的解。一個典型的B樹查找如下:

要查找某一滿足條件的點,先去找到滿足條件的線段,然後周遊所線上段上的點,即可找到答案。

B樹是一種相對來說比較複雜的資料結構,尤其是在它的删除與插入操作過程中,因為它涉及到了葉子結點的分解與合并。由于本文第一節已經詳細介紹了B樹和B+樹,下面直接開始介紹我們的第二個主角:R樹。

簡介

1984年,加州大學伯克利分校的Guttman發表了一篇題為“R-trees: a dynamic index structure for spatial searching”的論文,向世人介紹了R樹這種處理高維空間存儲問題的資料結構。本文便是基于這篇論文寫作完成的,是以如果大家對R樹非常有興趣,我想最好還是參考一下原著吧:)。為表示對這位牛人的尊重,給個引用先:

Guttman, A.; “R-trees: a dynamic index structure for spatial searching,” ACM, 1984, 14

R樹在資料庫等領域做出的功績是非常顯著的。它很好的解決了在高維空間搜尋等問題。舉個R樹在現實領域中能夠解決的例子吧:查找20英裡以内所有的餐廳。如果沒有R樹你會怎麼解決?一般情況下我們會把餐廳的坐标(x,y)分為兩個字段存放在資料庫中,一個字段記錄經度,另一個字段記錄緯度。這樣的話我們就需要周遊所有的餐廳擷取其位置資訊,然後計算是否滿足要求。如果一個地區有100家餐廳的話,我們就要進行100次位置計算操作了,如果應用到谷歌地圖這種超​​大資料​​庫中,我想這種方法肯定不可行吧。

R樹就很好的解決了這種高維空間搜尋問題。它把B樹的思想很好的擴充到了多元空間,采用了B樹分割空間的思想,并在添加、删除操作時采用合并、分解結點的方法,保證樹的平衡性。是以,R樹就是一棵用來存儲高維資料的平衡樹。

好了簡介就到此為止。以下,本文将詳細介紹R樹的資料結構以及R樹的操作。至于R樹的擴充與R樹的性能問題,我就僅僅在文末簡單介紹一下吧,這些問題最好查閱相關論文比較合适。

R樹的資料結構

如上所述,R樹是B樹在高維空間的擴充,是一棵平衡樹。每個R樹的葉子結點包含了多個指向不同資料的指針,這些資料可以是存放在硬碟中的,也可以是存在記憶體中。根據R樹的這種資料結構,當我們需要進行一個高維空間查詢時,我們隻需要周遊少數幾個葉子結點所包含的指針,檢視這些指針指向的資料是否滿足要求即可。這種方式使我們不必周遊所有資料即可獲得答案,效率顯著提高。下圖1是R樹的一個簡單執行個體:

我們在上面說過,R樹運用了空間分割的理念,這種理念是如何實作的呢?R樹采用了一種稱為MBR(Minimal Bounding Rectangle)的方法,在此我把它譯作“最小邊界矩形”。從葉子結點開始用矩形(rectangle)将空間框起來,結點越往上,框住的空間就越大,以此對空間進行分割。有點不懂?沒關系,繼續往下看。在這裡我還想提一下,R樹中的R應該代表的是Rectangle(此處參考wikipedia),而不是大多數國内教材中所說的Region(很多書把R樹稱為區域樹,這是有誤的)。我們就拿二維空間來舉例吧。下圖是Guttman論文中的一幅圖。

我來詳細解釋一下這張圖。先來看圖(b)吧。首先我們假設所有資料都是二維空間下的點,圖中僅僅标志了R8區域中的資料,也就是那個shape of data object。别把那一塊不規則圖形看成一個資料,我們把它看作是多個資料圍成的一個區域。為了實作R樹結構,我們用一個最小邊界矩形恰好框住這個不規則區域,這樣,我們就構造出了一個區域:R8。R8的特點很明顯,就是正正好好框住所有在此區域中的資料。其他實線包圍住的區域,如R9,R10,R12等都是同樣的道理。這樣一來,我們一共得到了12個最最基本的最小矩形。這些矩形都将被存儲在子結點中。下一步操作就是進行高一層次的處理。我們發現R8,R9,R10三個矩形距離最為靠近,是以就可以用一個更大的矩形R3恰好框住這3個矩形。同樣道理,R15,R16被R6恰好框住,R11,R12被R4恰好框住,等等。所有最基本的最小邊界矩形被框入更大的矩形中之後,再次疊代,用更大的框去框住這些矩形。我想大家都應該了解這個資料結構的特征了。用地圖的例子來解釋,就是所有的資料都是餐廳所對應的地點,先把相鄰的餐廳劃分到同一塊區域,劃分好所有餐廳之後,再把鄰近的區域劃分到更大的區域,劃分完畢後再次進行更高層次的劃分,直到劃分到隻剩下兩個最大的區域為止。要查找的時候就友善了吧。

下面就可以把這些大大小小的矩形存入我們的R樹中去了。根結點存放的是兩個最大的矩形,這兩個最大的矩形框住了所有的剩餘的矩形,當然也就框住了所有的資料。下一層的結點存放了次大的矩形,這些矩形縮小了範圍。每個葉子結點都是存放的最小的矩形,這些矩形中可能包含有n個資料。

在這裡,讀者先不要去糾結于如何劃分資料到最小區域矩形,也不要糾結怎樣用更大的矩形框住小矩形,這些都是下一節我們要讨論的。

講完了基本的資料結構,我們來講個執行個體,如何查詢特定的資料吧。又以餐廳為例吧。假設我要查詢廣州市天河區天河城附近一公裡的所有餐廳位址怎麼辦?打開地圖(也就是整個R樹),先選擇國内還是國外(也就是根結點)。然後選擇華南地區(對應第一層結點),選擇廣州市(對應第二層結點),再選擇天河區(對應第三層結點),最後選擇天河城所在的那個區域(對應葉子結點,存放有最小矩形),周遊所有在此區域内的結點,看是否滿足我們的要求即可。怎麼樣,其實R樹的查找規則跟查地圖很像吧?對應下圖:

一棵R樹滿足如下的性質:

1.     除非它是根結點之外,所有葉子結點包含有m至M個記錄索引(條目)。作為根結點的葉子結點所具有的記錄個數可以少于m。通常,m=M/2。

2.     對于所有在葉子中存儲的記錄(條目),I是最小的可以在空間中完全覆寫這些記錄所代表的點的矩形(注意:此處所說的“矩形”是可以擴充到高維空間的)。

3.     每一個飛葉子結點擁有m至M個孩子結點,除非它是根結點。

4.     對于在非葉子結點上的每一個條目,i是最小的可以在空間上完全覆寫這些條目所代表的店的矩形(同性質2)。

5.     所有葉子結點都位于同一層,是以R樹為平衡樹。

葉子結點的結構

先來探究一下葉子結點的結構吧。葉子結點所儲存的資料形式為:(I, tuple-identifier)。

      其中,tuple-identifier表示的是一個存放于資料庫中的tuple,也就是一條記錄,它是n維的。I是一個n維空間的矩形,并可以恰好框住這個葉子結點中所有記錄代表的n維空間中的點。I=(I0,I1,…,In-1)。其結構如下圖所示:

下圖描述的就是在二維空間中的葉子結點所要存儲的資訊。

在這張圖中,I所代表的就是圖中的矩形,其範圍是a<=I0<=b,c<=I1<=d。有兩個tuple-identifier,在圖中即表示為那兩個點。這種形式完全可以推廣到高維空間。大家簡單想想三維空間中的樣子就可以了。這樣,葉子結點的結構就介紹完了。

非葉子結點

      非葉子結點的結構其實與葉子結點非常類似。想象一下B+樹就知道了,B+樹的葉子結點存放的是真實存在的資料,而非葉子結點存放的是這些資料的“邊界”,或者說也算是一種索引(有疑問的讀者可以回顧一下上述第一節中講解B+樹的部分)。

      同樣道理,R樹的非葉子結點存放的資料結構為:(I, child-pointer)。

      其中,child-pointer是指向孩子結點的指針,I是覆寫所有孩子結點對應矩形的矩形。這邊有點拗口,但我想不是很難懂吧?給張圖:

D,E,F,G為孩子結點所對應的矩形。A為能夠覆寫這些矩形的更大的矩形。這個A就是這個非葉子結點所對應的矩形。這時候你應該悟到了吧?無論是葉子結點還是非葉子結點,它們都對應着一個矩形。樹形結構上層的結點所對應的矩形能夠完全覆寫它的孩子結點所對應的矩形。根結點也唯一對應一個矩形,而這個矩形是可以覆寫所有我們擁有的資料資訊在空間中代表的點的。

我個人感覺這張圖畫的不那麼精确,應該是矩形A要恰好覆寫D,E,F,G,而不應該再留出這麼多沒用的空間了。但為尊重原圖的繪制者,特不作修改。

R樹的操作

這一部分也許是程式設計者最關注的問題了。這麼高效的資料結構該如何去實作呢?這便是這一節需要闡述的問題。

搜尋

R樹的搜尋操作很簡單,跟B樹上的搜尋十分相似。它傳回的結果是所有符合查找資訊的記錄條目。而輸入是什麼?就我個人的了解,輸入不僅僅是一個範圍了,它更可以看成是一個空間中的矩形。也就是說,我們輸入的是一個搜尋矩形。

先給出僞代碼:

Function:Search

描述:假設T為一棵R樹的根結點,查找所有搜尋矩形S覆寫的記錄條目。

S1:[查找子樹] 如果T是非葉子結點,如果T所對應的矩形與S有重合,那麼檢查所有T中存儲的條目,對于所有這些條目,使用Search操作作用在每一個條目所指向的子樹的根結點上(即T結點的孩子結點)。

S2:[查找葉子結點] 如果T是葉子結點,如果T所對應的矩形與S有重合,那麼直接檢查S所指向的所有記錄條目。傳回符合條件的記錄。

我們通過下圖來了解這個Search操作。

陰影部分所對應的矩形為搜尋矩形。它與根結點對應的最大的矩形(未畫出)有重疊。這樣将Search操作作用在其兩個子樹上。兩個子樹對應的矩形分别為R1與R2。搜尋R1,發現與R1中的R4矩形有重疊,繼續搜尋R4。最終在R4所包含的R11與R12兩個矩形中查找是否有符合條件的記錄。搜尋R2的過程同樣如此。很顯然,該算法進行的是一個疊代操作。

插入

      R樹的插入操作也同B樹的插入操作類似。當新的資料記錄需要被添加入葉子結點時,若葉子結點溢出,那麼我們需要對葉子結點進行分裂操作。顯然,葉子結點的插入操作會比搜尋操作要複雜。插入操作需要一些輔助方法才能夠完成。

來看一下僞代碼:

Function:Insert

描述:将新的記錄條目E插入給定的R樹中。

I1:[為新記錄找到合适插入的葉子結點] 開始ChooseLeaf方法選擇葉子結點L以放置記錄E。

I2:[添加新記錄至葉子結點] 如果L有足夠的空間來放置新的記錄條目,則向L中添加E。如果沒有足夠的空間,則進行SplitNode方法以獲得兩個結點L與LL,這兩個結點包含了所有原來葉子結點L中的條目與新條目E。

I3:[将變換向上傳遞] 開始對結點L進行AdjustTree操作,如果進行了分裂操作,那麼同時需要對LL進行AdjustTree操作。

I4:[對樹進行增高操作] 如果結點分裂,且該分裂向上傳播導緻了根結點的分裂,那麼需要建立一個新的根結點,并且讓它的兩個孩子結點分别為原來那個根結點分裂後的兩個結點。

Function:ChooseLeaf

描述:選擇葉子結點以放置新條目E。

CL1:[Initialize] 設定N為根結點。

CL2:[葉子結點的檢查] 如果N為葉子結點,則直接傳回N。

CL3:[選擇子樹] 如果N不是葉子結點,則周遊N中的結點,找出添加E.I時擴張最小的結點,并把該結點定義為F。如果有多個這樣的結點,那麼選擇面積最小的結點。

CL4:[下降至葉子結點] 将N設為F,從CL2開始重複操作。

Function:AdjustTree

描述:葉子結點的改變向上傳遞至根結點以改變各個矩陣。在傳遞變換的過程中可能會産生結點的分裂。

AT1:[初始化] 将N設為L。

AT2:[檢驗是否完成] 如果N為根結點,則停止操作。

AT3:[調整父結點條目的最小邊界矩形] 設P為N的父節點,EN為指向在父節點P中指向N的條目。調整EN.I以保證所有在N中的矩形都被恰好包圍。

AT4:[向上傳遞結點分裂] 如果N有一個剛剛被分裂産生的結點NN,則建立一個指向NN的條目ENN。如果P有空間來存放ENN,則将ENN添加到P中。如果沒有,則對P進行SplitNode操作以得到P和PP。

AT5:[升高至下一級] 如果N等于L且發生了分裂,則把NN置為PP。從AT2開始重複操作。

同樣,我們用圖來更加直覺的了解這個插入操作。

    我們來通過圖分析一下插入操作。現在我們需要插入R21這個矩形。開始時我們進行ChooseLeaf操作。在根結點中有兩個條目,分别為R1,R2。其實R1已經完全覆寫了R21,而若向R2中添加R21,則會使R2.I增大很多。顯然我們選擇R1插入。然後進行下一級的操作。相比于R4,向R3中添加R21會更合适,因為R3覆寫R21所需增大的面積相對較小。這樣就在B8,B9,B10所在的葉子結點中插入R21。由于葉子結點沒有足夠空間,則要進行分裂操作。

    插入操作如下圖所示:

這個插入操作其實類似于第一節中B樹的插入操作,這裡不再具體介紹,不過想必看過上面的僞代碼大家應該也清楚了。

删除

R樹的删除操作與B樹的删除操作會有所不同,不過同B樹一樣,會涉及到壓縮等操作。相信讀者看完以下的僞代碼之後會有所體會。R樹的删除同樣是比較複雜的,需要用到一些輔助函數來完成整個操作。

僞代碼如下:

Function:Delete

描述:将一條記錄E從指定的R樹中删除。

D1:[找到含有記錄的葉子結點] 使用FindLeaf方法找到包含有記錄E的葉子結點L。如果搜尋失敗,則直接終止。

D2:[删除記錄] 将E從L中删除。

D3:[傳遞記錄] 對L使用CondenseTree操作

D4:[縮減樹] 當經過以上調整後,如果根結點隻包含有一個孩子結點,則将這個唯一的孩子結點設為根結點。

Function:FindLeaf

描述:根結點為T,期望找到包含有記錄E的葉子結點。

FL1:[搜尋子樹] 如果T不是葉子結點,則檢查每一條T中的條目F,找出與E所對應的矩形相重合的F(不必完全覆寫)。對于所有滿足條件的F,對其指向的孩子結點進行FindLeaf操作,直到尋找到E或者所有條目均以被檢查過。

FL2:[搜尋葉子結點以找到記錄] 如果T是葉子結點,那麼檢查每一個條目是否有E存在,如果有則傳回T。

Function:CondenseTree

描述:L為包含有被删除條目的葉子結點。如果L的條目數過少(小于要求的最小值m),則必須将該葉子結點L從樹中删除。經過這一删除操作,L中的剩餘條目必須重新插入樹中。此操作将一直重複直至到達根結點。同樣,調整在此修改樹的過程所經過的路徑上的所有結點對應的矩形大小。

CT1:[初始化] 令N為L。初始化一個用于存儲被删除結點包含的條目的連結清單Q。

CT2:[找到父條目] 如果N為根結點,那麼直接跳轉至CT6。否則令P為N 的父結點,令EN為P結點中存儲的指向N的條目。

CT3:[删除下溢結點] 如果N含有條目數少于m,則從P中删除EN,并把結點N中的條目添加傳入連結表Q中。

CT4:[調整覆寫矩形] 如果N沒有被删除,則調整EN.I使得其對應矩形能夠恰好覆寫N中的所有條目所對應的矩形。

CT5:[向上一層結點進行操作] 令N等于P,從CT2開始重複操作。

CT6:[重新插入孤立的條目] 所有在Q中的結點中的條目需要被重新插入。原來屬于葉子結點的條目可以使用Insert操作進行重新插入,而那些屬于非葉子結點的條目必須插入删除之前所在層的結點,以確定它們所指向的子樹還處于相同的層。

      R樹删除記錄過程中的CondenseTree操作是不同于B樹的。我們知道,B樹删除過程中,如果出現結點的記錄數少于半滿(即下溢)的情況,則直接把這些記錄與其他葉子的記錄“融合”,也就是說兩個相鄰結點合并。然而R樹卻是直接重新插入。

同樣,我們用圖直覺的說明這個操作。

假設結點最大條目數為4,最小條目數為2。在這張圖中,我們的目标是删除記錄c。首先使用FindLeaf操作找到c所處在的葉子結點的位置——R11。當c從R11删除時,R11就隻有一條記錄了,少于最小條目數2,出現下溢,此時要調用CondenseTree操作。這樣,c被删除,R11剩餘的條目——指向記錄d的指針——被插傳入連結表Q。然後向更高一層的結點進行此操作。這樣R12會被插傳入連結表中。原理是一樣的,在這裡就不再贅述。

有一點需要解釋的是,我們發現這個删除操作向上傳遞之後,根結點的條目R1也被插入了Q中,這樣根結點隻剩下了R2。别着急,重新插入操作會有效的解決這個問題。我們插入R3,R12,d至它原來所處的層。這樣,我們發現根結點隻有一個條目了,此時根據Inert中的操作,我們把這個根結點删除,它的孩子結點,即R5,R6,R7,R3所在的結點被置為根結點。至此,删除操作結束。

結語

      R樹是一種能夠有效進行高維空間搜尋的資料結構,它已經被廣泛應用在各種資料庫及其相關的應用中。但R樹的處理也具有局限性,它的最佳應用範圍是處理2至6維的資料,更高維的存儲會變得非常複雜,這樣就不适用了。近年來,R樹也出現了很多變體,R*樹就是其中的一種。這些變體提升了R樹的性能,感興趣的讀者可以參考相關文獻。文章有任何錯誤,還望各位讀者不吝賜教。本文完。

參考文獻以及相關網址:

1.   Organization and Maintenance of Large Ordered Indices

2.   the ubiquitous B tree

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