查詢:B-樹
前言:
動態查詢樹主要有:二叉查詢樹(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這種外存資料結構。
當磁碟驅動器執行讀/寫功能時。碟片裝在一個主軸上,並繞主軸高速旋轉,當磁軌在讀/寫頭(又叫磁頭) 下通過時,就可以進行資料的讀 / 寫了。
當碟片繞主軸旋轉的時候,磁頭與旋轉的碟片形成一個圓柱體。各個盤面上半徑相同的磁軌組成了一個圓柱面,我們稱為柱面 。因此,柱面的個數也就是盤面上的磁軌數。
磁碟的讀/寫原理和效率
磁碟上資料必須用一個三維地址唯一標示:柱面號、盤面號、塊號(磁軌上的盤塊)。
磁碟讀/寫步驟:
- 首先移動臂根據柱面號使磁頭移動到所需要的柱面上,這一過程被稱為定位或查詢 。
- 如上圖所示的4盤組示意圖中,所有磁頭都定位到了6個盤面的6條磁軌上(磁頭都是雙向的)。這時根據盤面號來確定指定盤面上的磁軌。
- 盤面確定以後,碟片開始旋轉,將指定塊號的磁軌段移動至磁頭下。
經過上面三個步驟,指定資料的儲存位置就被找到。這時就可以開始讀/寫操作了。
磁碟讀/寫時間組成:
- 查詢時間(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
總結:
每次磁碟的讀寫,IO代價主要花費在查詢時間Ts上,所以,在大規模資料儲存方面,大量資料儲存在外存磁碟中,而在外存磁碟中讀取/寫入塊(block)中某資料時,首先需要定位到磁碟中的某塊,如何有效地查詢磁碟中的資料,需要一種合理高效的外存資料結構,就是下面所要重點闡述的B-tree結構,以及相關的變種結構:B+-tree結構和B*-tree結構。
B-樹
具體講解之前,有一點,再次強調下:B-樹,即為B樹。因為B樹的原英文名稱為B-tree,而國內很多人喜歡把B-tree譯作B-樹,其實,這是個非常不好的直譯,很容易讓人產生誤解。如人們可能會以為B-樹是一種樹,而B樹又是一種一種樹。而事實上是,B-tree就是指的B樹。特此說明。
B樹是為了磁碟或其它儲存裝置而設計的一種多叉平衡查詢樹,與紅黑樹很相似。但是也存在一些不同。B樹與紅黑樹最大的不同在於,B樹的結點可以有多個子女,從幾個到上千個。那為什麼又說B樹與紅黑樹很相似呢?因為與紅黑樹一樣,一棵含n個結點的B樹的高度也為O(lgn),但可能比一棵紅黑樹的高度小許多,應為它的分支因子比較大。所以,B樹可以在O(logn)時間內,實現各種如插入(insert),刪除(delete)等動態集合操作。
B樹的定義:
用m階表示(上限):(上圖為一顆4階B樹)
- 每一個結點最多有m-1個關鍵字(上圖最多有3個);所以它最多含有m個孩子(m>=2).
- 除根結點和葉子結點外,每一個結點的關鍵字最少有[ceil(m / 2)]-1個(上圖最少有1個);所以它最少含有[ceil(m / 2)]個孩子(其中ceil(x)是一個取上限的函式);
- 若根結點不是葉子結點,則至少有2個孩子(特殊情況:沒有孩子的根結點,即根結點為葉子結點,整棵樹只有一個根節點);
- 所有葉子結點都出現在同一層,葉子結點不包含任何關鍵字資訊(可以看做是外部接點或查詢失敗的接點,實際上這些結點不存在,指向這些結點的指標都為null);
- 每個非終端結點中包含有n個關鍵字資訊: (n,P0,K1,P1,K2,P2,……,Kn,Pn)。其中:
- Ki (i=1…n)為關鍵字,且關鍵字按順序升序排序K(i-1)< Ki。
- Pi為指向子樹根的接點,且指標P(i-1)指向子樹種所有結點的關鍵字均小於Ki,但都大於K(i-1)。
- 關鍵字的個數n必須滿足: [ceil(m / 2)-1]<= n <= m-1。
用度表示(下限):(上圖為一顆度為2的B樹)
B樹中每一個結點能包含的關鍵字數有一個上界和下界。這個下界可以用一個稱作B樹的最小度數(演算法導論中文版上譯作度數,最小度數即內節點中節點最小孩子數目)t(t>=2)表示。
- 每個結點可包含至多2t-1個關鍵字(上圖最多有3個)。所以一個內結點至多可有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樹的高度:
對於輔存做IO讀的次數取決於B樹的高度。而B樹的高度由什麼決定的呢?
若B樹某一非葉子節點包含N個關鍵字,則此非葉子節點含有N+1個孩子結點,而所有的葉子結點都在第I層,我們可以得出:
- 因為根至少有兩個孩子,因此第2層至少有兩個結點。
- 除根和葉子外,其它結點至少有 t 個孩子,
- 因此在第3層至少有2*t個結點,
- 在第4層至少有
2t2 個結點, - 在第 I 層至少有
2tl−2 個結點; - 當B樹有n個關鍵字,那它的最大高度有:
n≥2th−1 ,(根節點為第0層)
B樹插入:
- 若該結點中關鍵碼個數小於m-1,則直接插入即可
- 若該結點中關鍵碼個數等於m-1,則將引起結點的分裂。以中間關鍵碼為界將結點一分為二,產生一個新結點,並把關鍵中間碼插入到父結點(h-1)層中
- 重複上述工作,最壞情況一直分裂到根結點,建立一個新的根結點,整個B樹增加一層
以下所有圖M值取3。
樹為空時
插入20時,對已有鍵值10和20進行比較,按照從左到右從小到大的順序插入。
當20插入根節點以後,節點size等於M,此時需要對節點進行分裂。若不分裂,則該節點孩子為四個,違反了性質2。這裡節點結構定義時多給了一格,以便插入鍵值時鍵值陣列不會越界。
分裂過程如下圖:
分裂時,建立兩個新節點,一個作為根節點用以存放節點中間鍵值為20的節點,一個用來存放中間鍵值右邊的所有鍵值,其次,更新孩子雙親指向關係。
樹不空
依次插入40和50,自上而下查詢插入位置,根據大小排列,插入30所在節點,50插入後需要再次分裂節點,此時,因該節點非根節點,則,分裂時,將中間鍵值之後的鍵值移入新節點中,中間鍵值存入雙親節點中,在此例中,其雙親為根節點,往雙親插入中間鍵值時,按照從左向右,從小到大的順序,即鍵值的插入順序。
再次插入80,70。圖示如下:分裂圖示如下:
上圖中,70插入後,該節點需要分裂,分裂完畢之後,70存入根節點,此時根節點也需要分裂,以滿足B樹性質。需要注意的是,分裂過程中,各個節點的孩子雙親指向需要及時更改,否則出錯,具體細節見程式碼實現。
B樹刪除:
首先,查詢B樹中需要刪除的元素,如果該元素在B樹中存在,則將該元素在其結點中進行刪除,如果刪除該元素後,首先判斷該元素是否有左右孩子結點,如果有,則上移孩子結點中的某相近元素到父結點中,然後是移動之後的情況;如果沒有,直接刪除後,然後是移動之後的情況
刪除元素,移動相應元素之後的處理過程
- 如果某結點中元素陣列(即關鍵字數)小於ceil(m/2)-1,則需要看其某相鄰兄弟結點是否豐滿(結點中的元素個數大於ceil(m/2)-1)
- 如果豐滿,則向父結點借一個元素來滿足條件
- 如果其相鄰兄弟都剛脫貧,即借了之後其結點數目小於ceil(m/2)-1,則該結點與其相鄰某一兄弟結點進行“合併”成一個結點,以此來滿足條件
下面看圖解:一棵5階B樹(樹中每個結點最多有4個元素,最少有2個元素)
B樹初始狀態
依次刪除H,T,R,E
刪除H結點,首先查詢H結點,H在一個葉子結點中,且該葉子結點元素數目大於3大於最小的元素數目2,則操作很簡單,只需要移動K至原來H的位置,移動L至原來K的位置(也就是結點中刪除元素後面的元素依次向前移動)
刪除元素T
刪除元素T,元素T沒有在葉子結點中,而是在中間結點中找到,發現他的繼承者W,將W上移到T的位置,然後將原包含W的孩子結點中的W進行刪除,這裡恰好刪除W後,該孩子結點中元素個數大於2,無需進行合併操作
刪除元素R
刪除元素R,R在葉子結點中,但是該結點中元素數目為2,刪除導致只有1個元素,已經小於最小元素數目ceil(5/2)-1=2,根據移動後的方案我們知道:如果其某個相鄰兄弟結點中比較豐滿,則可以向父結點借一個元素,然後將最豐滿的相鄰兄弟結點中上移最後或最前一個元素到父結點中,在這個例子中,右相鄰兄弟結點比較豐滿,所以先向父結點借一個元素W下移到葉子結點中,替代原來S的位置,同時S前移;然後X在相鄰右兄弟結點中上移到父結點中,最後在相鄰右兄弟結點中刪除X,後面元素前移
刪除元素E
刪除元素E,刪除後會導致很多問題,因為E所在的結點數目剛好達標,剛好滿足最小元素個數2,而相鄰的兄弟結點也是同樣的情況,刪除一個元素都不能滿足條件,所以需要該結點與某相鄰兄弟結點進行合併操作;首先,移動父結點的元素(該元素在兩個需要合併的結點元素之間)下移到其子結點中,然後將這兩個結點合併稱一個結點,所以在該例項中,首先將父結點中的元素D下移到已經刪除E而只有F的結點中,然後含有D、F的結點和含有A、C的結點進行合併成為一個結點
但是,這樣並沒有結束,因為父結點只包含一個元素G,不滿足5階B樹每個結點至少有2個關鍵字的特性。如果這個問題結點的相鄰兄弟結點比較豐滿,則可以向父結點借一個元素。假設這時右兄弟結點(含有Q、X)有兩個以上的元素,可以把M下移到元素少的結點,將Q上移到M的位置,這時,Q的左子樹變成含有G、M的樹。但是這個例項中,相鄰結點都正好脫貧,所以,只能與兄弟結點合併成一個結點,而根節點中唯一的元素M下移到子結點,這樣,樹的高度減少一層。