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

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

 

作者:July、weedge、Frankie。編程藝術室出品。

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

出處:http://blog.csdn.net/v_JULY_v 

 

第一節、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樹的各種操作能使B樹保持較低的高度,從而達到有效避免磁盤過於頻繁的查找存取操作,從而有效提高查找效率)。

B-tree(B-tree樹即B樹,B即Balanced,平衡的意思)這棵神奇的樹是在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磁盤的構造

磁盤是一個扁平的圓盤(與電唱機的唱片類似)。盤面上有許多稱爲磁道的圓圈,數據就記錄在這些磁道上。磁盤可以是單片的,也可以是由若干盤片組成的盤組,每一盤片上有兩個面。如下圖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- 樹 

     3.1什麼是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樹的定義,從下文中,你將看到,或者是用階,或者是用度,如下段文字所述:
    Unfortunately, the literature on B-trees is not uniform in its use of terms relating to B-Trees. (Folk & Zoellick 1992, p. 362) Bayer & McCreight (1972), Comer (1979), and others define the order of B-tree as the minimum number of keys in a non-root node. Folk & Zoellick (1992) points out that terminology is ambiguous because the maximum number of keys is not clear. An order 3 B-tree might hold a maximum of 6 keys or a maximum of 7 keys. (Knuth 1998,TAOCP p. 483) avoids the problem by defining the order to be maximum number of children (which is one more than the maximum number of keys).

    from: http://en.wikipedia.org/wiki/Btree#Technical_description

    用階定義的B樹

    B 樹又叫平衡多路查找樹。一棵m階的B 樹 (注:切勿簡單的認爲一棵m階的B樹是m叉樹,雖然存在四叉樹八叉樹KD,及vp/R樹/R*樹/R+樹/X樹/M樹/線段樹/希爾伯特R樹/優先R樹等空間劃分樹,但與B樹完全不等同)的特性如下

  1. 樹中每個結點最多含有m個孩子(m>=2);

  2. 除根結點和葉子結點外,其它每個結點至少有[ceil(m / 2)]個孩子(其中ceil(x)是一個取上限的函數);

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

  4. 所有葉子結點都出現在同一層,葉子結點不包含任何關鍵字信息(可以看做是外部接點或查詢失敗的接點,實際上這些結點不存在,指向這些結點的指針都爲null);(讀者反饋@冷嶽:這裏有錯,葉子節點只是沒有孩子和指向孩子的指針,這些節點也存在,也有元素。@研究者July:其實,關鍵是把什麼當做葉子結點,因爲如紅黑樹中,每一個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。如下圖所示:

    用度定義的B樹

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

  • 每個非根的內結點至多有m個子女,每個非根的結點必須至少含有m-1個關鍵字,如果樹是非空的,則根結點至少包含一個關鍵字;

  • 每個結點可包含至多2m-1個關鍵字。所以一個內結點至多可有2m個子女。如果一個結點恰好有2m-1個關鍵字,我們就說這個結點是滿的(而稍後介紹的B*樹作爲B樹的一種常用變形,B*樹中要求每個內結點至少爲2/3滿,而不是像這裏的B樹所要求的至少半滿);

  • 當關鍵字數m=2(t=2的意思是,mmin=2,m可以>=2)時的B樹是最簡單的有很多人會因此誤認爲B樹就是二叉查找樹,但二叉查找樹就是二叉查找樹,B樹就是B樹,B樹是一棵含有m(m>=2)個關鍵字的平衡多路查找樹,此時,每個內結點可能因此而含有2個、3個或4個子女,亦即一棵2-3-4樹,然而在實際中,通常採用大得多的t值。

    B樹中的每個結點根據實際情況可以包含大量的關鍵字信息和分支(當然是不能超過磁盤塊的大小,根據磁盤驅動(disk drives)的不同,一般塊的大小在1k~4k左右);這樣樹的深度降低了,這就意味着查找一個元素只要很少結點從外存磁盤中讀入內存,很快訪問到要查找的數據。如果你看完上面關於B樹定義的介紹,思維感覺不夠清晰,請繼續參閱下文第6小節、B樹的插入、刪除操作 部分。

    3.2B樹的類型和節點定義

    B樹的類型和節點定義如下圖所示:

 

 

    3.3文件查找的具體過程(涉及磁盤IO操作)

爲了簡單,這裏用少量數據構造一棵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次內存查找操作。關於內存中的文件名查找,由於是一個有序表結構,可以利用折半查找提高效率。至於IO操作是影響整個B樹查找效率的決定因素。

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

 

3.4B樹的高度

    根據上面的例子我們可以看出,對於輔存做IO讀的次數取決於B樹的高度。而B樹的高度由什麼決定的呢?

    若B樹某一非葉子節點包含N個關鍵字,則此非葉子節點含有N+1個孩子結點,而所有的葉子結點都在第I層,我們可以得出:

  1. 因爲根至少有兩個孩子,因此第2層至少有兩個結點。
  2. 除根和葉子外,其它結點至少有┌m/2┐個孩子,
  3. 因此在第3層至少有2*┌m/2┐個結點,
  4. 在第4層至少有2*(┌m/2┐^2)個結點,
  5. 在第 I 層至少有2*(┌m/2┐^(l-2) )個結點,於是有: N+1 ≥ 2*┌m/2┐I-2;
  6. 考慮第L層的結點個數爲N+1,那麼2*(┌m/2┐^(l-2))≤N+1,也就是L層的最少結點數剛好達到N+1個,即: I≤ log┌m/2┐((N+1)/2 )+2;

  所以

  • 當B樹包含N個關鍵字時,B樹的最大高度爲l-1(因爲計算B樹高度時,葉結點所在層不計算在內),即:l - 1 = log┌m/2┐((N+1)/2 )+1

  這個B樹的高度公式從側面顯示了B樹的查找效率是相當高的。

曾在一次面試中被問到,一棵含有N個總關鍵字數的m階的B樹的最大高度是多少?答曰:log_ceil(m/2)(N+1)/2 + 1 (上面中關於m階B樹的第1點特性已經提到:樹中每個結點含有最多含有m個孩子,即m滿足:ceil(m/2)<=m<=m。而樹中每個結點含孩子數越少,樹的高度則越大,故如此)。在2012微軟4月份的筆試中也問到了此問題。

此外,還有讀者反饋,說上面的B樹的高度計算公式與算法導論一書上的不同,而後我特意翻看了算法導論第18章關於B樹的高度一節的內容,如下圖所示:

在上圖中書上所舉的例子中,也許,根據我們大多數人的理解,它的高度應該是4,而書上卻說的是“一棵高度爲3的B樹”。我想,此時,你也就明白了,算法導論一書上的高度的定義是從“0”開始計數的,而我們中國人的習慣是樹的高度是從“1”開始計數的。特此說明。July、二零一二年九月二十七日。

 

4.B+-tree

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

一棵m階的B+樹和m階的B樹的異同點在於:

      1.有n棵子樹的結點中含有n-1 個關鍵字; (此處頗有爭議,B+樹到底是與B 樹n棵子樹有n-1個關鍵字 保持一致,還是不一致:B樹n棵子樹的結點中含有n個關鍵字,待後續查證。暫先提供兩個參考鏈接:①wikipedia http://en.wikipedia.org/wiki/B%2B_tree#Overview;②http://hedengcheng.com/?p=525。而下面B+樹的圖尚未最終確定是否有問題,請讀者注意)

      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的查詢效率更加穩定

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

讀者點評
本文評論下第149樓,fanyy1991針對上文所說的兩點,道:個人覺得這兩個原因都不是主要原因。數據庫索引採用B+樹的主要原因是 B樹在提高了磁盤IO性能的同時並沒有解決元素遍歷的效率低下的問題。正是爲了解決這個問題,B+樹應運而生。B+樹只要遍歷葉子節點就可以實現整棵樹的遍歷。而且在數據庫中基於範圍的查詢是非常頻繁的,而B樹不支持這樣的操作(或者說效率太低)。

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

 

 

5.B*-tree

B*-tree是B+-tree的變體,在B+樹的基礎上(所有的葉子結點中包含了全部關鍵字的信息,及指向含有這些關鍵字記錄的指針),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)基本操作進行詳細的介紹。

但在此之前,咱們還得簡單回顧下一棵m階的B 樹的特性,如下:

  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階(即樹中任一結點至多含有4個關鍵字,5棵子樹)B樹實例進行講解(如下圖所示):

備註:關鍵字數(2-4個)針對--非根結點(包括葉子結點在內),孩子數(3-5個)--針對根結點和葉子結點之外的內結點。當然,根結點是必須至少有2個孩子的,不然就成直線型搜索樹了。下圖中,讀者可以看到關鍵字數2-4個,內結點孩子數3-5個:

關鍵字爲大寫字母,順序爲字母升序。

結點定義如下:

typedef struct{

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

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

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

} NodeType;

 

6.1、插入(insert)操作

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

1、OK,下面咱們通過一個實例來逐步講解下。插入以下字符字母到一棵空的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樹就是一棵用來存儲高維數據的平衡樹。

OK,接下來,本文將詳細介紹R樹的數據結構以及R樹的操作。至於R樹的擴展與R樹的性能問題,可以查閱相關論文。

 

R樹的數據結構

如上所述,R樹是B樹在高維空間的擴展,是一棵平衡樹。每個R樹的葉子結點包含了多個指向不同數據的指針,這些數據可以是存放在硬盤中的,也可以是存在內存中。根據R樹的這種數據結構,當我們需要進行一個高維空間查詢時,我們只需要遍歷少數幾個葉子結點所包含的指針,查看這些指針指向的數據是否滿足要求即可。這種方式使我們不必遍歷所有數據即可獲得答案,效率顯著提高。下圖1是R樹的一個簡單實例:

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

 

我來詳細解釋一下這張圖。先來看圖(b)

 

  1. 首先我們假設所有數據都是二維空間下的點,圖中僅僅標誌了R8區域中的數據,也就是那個shape of data object。別把那一塊不規則圖形看成一個數據,我們把它看作是多個數據圍成的一個區域。爲了實現R樹結構,我們用一個最小邊界矩形恰好框住這個不規則區域,這樣,我們就構造出了一個區域:R8。R8的特點很明顯,就是正正好好框住所有在此區域中的數據。其他實線包圍住的區域,如R9,R10,R12等都是同樣的道理。這樣一來,我們一共得到了12個最最基本的最小矩形。這些矩形都將被存儲在子結點中。
  2. 下一步操作就是進行高一層次的處理。我們發現R8,R9,R10三個矩形距離最爲靠近,因此就可以用一個更大的矩形R3恰好框住這3個矩形。
  3. 同樣道理,R15,R16被R6恰好框住,R11,R12被R4恰好框住,等等。所有最基本的最小邊界矩形被框入更大的矩形中之後,再次迭代,用更大的框去框住這些矩形。

 

我想大家都應該理解這個數據結構的特徵了。用地圖的例子來解釋,就是所有的數據都是餐廳所對應的地點,先把相鄰的餐廳劃分到同一塊區域,劃分好所有餐廳之後,再把鄰近的區域劃分到更大的區域,劃分完畢後再次進行更高層次的劃分,直到劃分到只剩下兩個最大的區域爲止。要查找的時候就方便了。

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

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

講完了基本的數據結構,我們來講個實例,如何查詢特定的數據。又以餐廳爲例,假設我要查詢廣州市天河區天河城附近一公里的所有餐廳地址怎麼辦?

 

  1. 打開地圖(也就是整個R樹),先選擇國內還是國外(也就是根結點)。
  2. 然後選擇華南地區(對應第一層結點),選擇廣州市(對應第二層結點),
  3. 再選擇天河區(對應第三層結點),
  4. 最後選擇天河城所在的那個區域(對應葉子結點,存放有最小矩形),遍歷所有在此區域內的結點,看是否滿足我們的要求即可。

 

怎麼樣,其實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所需增大的面積相對較小。這樣就在R8,R9,R10所在的葉子結點中插入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 (給出了國外一些開源地址)

4.   http://en.wikipedia.org/wiki/Btree#Technical_description

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

6.   http://slady.net/java/bt/view.php(如果瞭解了B-tree結構,該地址可以在線對該結構進行查找(search),插入(insert),刪除(delete)操作。)
7. Guttman, A.; “R-trees: a dynamic index structure for spatial searching,” ACM, 1984, 14

8. http://www.cnblogs.com/CareySon/archive/2012/04/06/2435349.html

9. http://baike.baidu.com/view/298408.htm

10. http://www.cnblogs.com/leoo2sk/archive/2011/07/10/mysql-index.html (介紹了mysql中myisam和innodb這兩種引擎的內部索引機制,以及對不同字段的索引時,檢索效率上的對比,主要也是基於其內部機制的理解)

11. http://www.oschina.net/news/31988/mysql-indexing-best-practices (MySQL 索引最佳實踐);

12. http://idlebox.net/2007/stx-btree/(此頁面包含B樹生成構造的一些演示demo)。

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