> 《算法導論》學習
基本介紹
B樹是爲磁盤或者其他直接存取的輔助存儲(secondary storage)設備而設計的平衡搜索樹。B樹類似於紅黑樹,但是在降低磁盤I/O操作數方面表現更好。許多數據庫系統使用B樹或其變種來存儲信息。
一個典型的磁盤驅動器如圖所示,從存儲結構中讀取某段信息的步驟如下:
①查看主存(primary memory/main memory),假如對象在主存中,則可以向平常一樣引用該對象;
②否則該對象存儲在磁盤上,需要先從磁盤讀入主存;
③將對該對象的修改保存到磁盤中。
爲了加快數據的讀寫,我們需要每次的讀寫操作都能讀取到儘可能多的信息,減少讀取操作。一個B樹節點通常和一個完整磁盤頁一樣大,並且磁盤頁的大小限制了一個B樹節點可以含有的孩子個數。一個大的分支因子可以大大地降低樹的高度以及查找任何一個關鍵字所需的磁盤存儲次數。
B樹定義
首先需要區分對於一棵B樹度與階:
最小度數(minmum degree)是一個固定整數表示B樹中一個節點所包含孩子節點個數的下界。
階(order)表示B樹中一個節點所包含孩子節點個數上界。
一棵B樹T是具有以下性質的有根樹:
1. 每個節點具有屬性:(1) x.n,當前存儲在節點x中的關鍵字個數;(2)x.n個關鍵字本身的
2. 每個內部節點x還包含x.n+1個指向其孩子的指針
3. 關鍵字
4. 每個葉節點具有相同的深度,即樹的高度h。
5. 每個節點所包含的關鍵字個數有上界和下界。用一個被稱爲B樹的最小度數的固定整數
B樹上的基本操作
一、插入、分裂關鍵字
B樹插入一個關鍵字比較複雜,不能簡單的創建一個新的葉節點然後將其插入,因爲此時得到的不再是是合法的B樹。通常,我們是將新的關鍵字插入到一個已經存在的葉節點上。由於不能將關鍵字插入一個滿的葉節點,所以需要引入一個操作,將一個滿的節點y(有2t-1個關鍵字)按照其中間關鍵字
(算法導論的示例圖好像有點問題,所以自己畫了一個,如果大佬看出是我錯了,請告訴我)
/* ================== 分裂節點 ================= */
/* x: 被分裂節點的父節點 */
/* y: 被分裂節點,是x的第i個孩子 */
/* z: x的新孩子取走y的後t-1個關鍵字及相應的t個孩子 */
/* */
B-TREE-SPLIT-CHILD(x, i)
/* 創建節點z */
z = ALLOCATE-NODE()
y = x.c(i)
z.leaf = y.leaf
z.n = t-1
for j=1 to t-1
z.key(j) = y.key(j+t)
if not y.leaf /* 假如y爲內部節點 */
for j = 1 to t
z.c(j) = y.c(j+t)
y.n = t-1 /* 調整y的關鍵字個數 */
/* 將z插入爲x的一個孩子 */
for j=x.n+1 downto i+1
x.c(j+1) = x.c(j)
x.c(i+1) = z
for j=x.n downto i
x.key(j+1) = x.key(i)
x.key(i) = y.key(t)
x.n = x.n + 1
DISK-WRITE(y)
DISK-WRITE(z)
DISK-WRITE(x)
在一棵高度爲h的B樹T中,以沿樹單程下行的方式插入一個關鍵字k的操作需要O(h)次磁盤存取。所需要的CPU時間爲
/* ================== 插入節點 ================= */
/* T: 插入節點的樹 */
/* k: 插入的關鍵字 */
/* s: 當根節點爲滿時創建的新的根節點 */
/* r: 原先的根節點 */
/* */
B-TREE-INSERT(T, k)
r = T.root
if r.n == 2t-1 /* 根節點爲滿 */
s = ALLOCATE-NODE()
T.root = s
s.leaf = FALSE
s.n = 0
s.c(1) = r
B-TREE-SPLIT-CHILD(s, 1)
B-TREE-INSERT-NONFULL(s, k)
else
B-TREE-INSERT-NONFULL(r, k)
/* ============== 輔助的遞歸過程 ================ */
/* x: 關鍵字想要插入的節點 */
/* k: 插入的關鍵字 */
/* */
B-TREE-INSERT-NONFULL(x,k)
i = x.n
if x.leaf
while i>=1 and k<x.key(i)
x.key(i+1) = x.key(i)
i = i-1
x.key(i+1) = k
x.n = x.n + 1
DISK-WRITE(x)
else while i>=1 and k<x.key(i)
i = i-1
i = i+1
DISK-READ(x.c(i))
if x.c(i).n == 2t-1
B-TREE-SPLIT-CHILD(x, i)
if k > x.key(i)
i = i+1
B-TREE-INSERT-NONFULL(x.c(i), k)
二、刪除關鍵字
刪除操作也需要特殊處理,當從一個內部節點刪除一個關鍵字時,還要重新安排這個節點的孩子。同時與插入操作類似,當刪除一個節點時,必須保證一個結點不會在刪除其間變得太小(根節點除外,因爲根節點允許有比最少關鍵數t-1還少的關鍵字個數),當要刪除關鍵字路徑上節點(非根)有最少的關鍵字個數時,也可能需要向上回溯。
過程B-TREE-DELETE從以x爲根的子樹中刪除關鍵字k。該過程保證無論何時,節點x遞歸調用自身時,x中關鍵字個數至少爲最小度數t。注意到,這個條件要求比通常B樹中的最少關鍵字個數多一個以上,使得有時在遞歸下降至子節點之前,需要把一個關鍵字移到子節點中。
分情況討論:
1. 情況1:若關鍵字k在節點x中,並且x是葉節點,則從x中刪除k。
2. 情況2:若關鍵字k在節點x中,並且x是內部節點。則進行以下操作:(a). 如果節點x中前於k的子節點y至少包含t個關鍵字,則找出k在以y爲根的子樹中的前驅k’。遞歸的刪除k’,並在x中用k’替代k(找到k’並刪除它可在沿樹下降的單過程中完成)。(b). 類似(a),若y有少於t個關鍵字,則檢查節點x中後於k的子節點z。如果z至少有t個關鍵字,則找出k在以z爲根的子樹中的後繼k’。遞歸的刪除k’,並在x中用k’代替k(找到k’並刪除它可在沿樹下降的單過程中完成)。(c). 否則,若y和z都只含有t-1個關鍵字,則將k和z的全部合併進y,這樣x就失去了k和指向z的指針,並且y現在含有2t-1個關鍵字,然後釋放z遞歸的從y中刪除k。
3. 若關鍵字k不在內部節點x中,則確定必包含k的子樹的根
僞代碼如下所示,爲個人思路,如有錯誤請指出:
/* ================== 合併節點 ================= */
/* 合併的兩個子節點的父節點 */
/* index: 被合併的節點的左節點 */
/* */
B-TREE-MERGE-CHILD(x, index)
pchild1 = x.c(index)
pchild2 = x.c(index+1)
pchild1.key(t) = x.key(index) /* 父節點index位置的關鍵字下降 */
for j=1 to t-1 /* 將右兄弟節點關鍵字合併到左兄弟節點 */
pchild1.key(j+t) = pchild2.key(j)
if !pchild1.leaf
for j=1 to t-1 /* 將右兄弟節點孩子合併到左兄弟節點 */
pchild1.c(j+t-1) = pchild2.c(j)
pchild1.n = 2t-1
for j=index to x.n /* 父節點刪除一個關鍵字,index後前移一位 */
x.key(j) = x.key(j+1)
x.c(j+1) = x.c(j+2)
delete pchild2
if(x->n == 0) delete x
/* ================== 刪除節點 ================= */
/* T: 刪除節點的樹 */
/* k: 刪除的關鍵字 */
/* rchild1: 當根節點只有一個關鍵字時的左孩子 */
/* rchild2: 當根節點只有一個關鍵字時的右孩子 */
/* */
B-TREE-DELETE(T, k)
/* 注意:需要確認該關鍵字確實存在 */
r = T.root
if r.n==1
if r.leaf /* 只有一個節點的樹,需要刪除根 */
delete r
T.root = NIL
else /* case 3b,根據插入規則,可知此時若有孩子只可能有兩個孩子 */
rchild1 = r.c(1)
rchild2 = r.c(2)
if rchild1.n==t-1 and rchild2.n==t-1
B-TREE-MERGE-CHILD(r, 0)
delete r
T.root = rchild1
B-TREE-DELETE-RECURSIVE(r, k)
/* ============== 輔助的遞歸過程 ================ */
/* x: 刪除的關鍵字所在子樹的根節點 */
/* k: 刪除的關鍵字 */
/* y: 節點x中前於k的子節點y */
/* z: 節點x中後於k的子節點z */
/* */
B-TREE-DELETE-RECURSIVE(x, k)
i = 1
while i<=x.n and k>x.key(i)
i = i+1
if i<=x.n and k==x.key(i) /* 關鍵字k位於節點x */
if x.leaf /* case 1 */
for j=i to x.n-1
x.key(j) = x.key(j+1)
x.n = x.n-1
else /* case 2 */
if y.n>=t /* case 2a */
k1 = B-TREE-GET-PREDECESSOR(y)
B-TREE-DELETE-RECURSIVE(y, k1)
x.key(i) = k1
return
else if z.n>=t /* case 2b */
k1 = B-TREE-GET-PREDECESSOR(z)
B-TREE-DELETE-RECURSIVE(z, k1)
x.key(i) = k1
else /* case 2c */
B-TREE-MERGE-CHILD(x, i)
B-TREE-DELETE-RECURSIVE(y, k)
else if i<=x.n and k<x.key(i) /* 關鍵字k不在節點x */
child = x.c(i) /* 關鍵字所在的子樹根節點 */
if child.n==t-1
pLeft = NULL /* child的左兄弟節點 */
pRight = NULL /* child的右兄弟節點 */
if i>0
pLeft = x.c(i-1)
if i<x.n
pRight = x.c(i+1)
if pLeft and pLeft.n>=t /* case 3a: 將左兄弟節點的一個關鍵字挪到child節點 */
/* 父節點的第i-1個關鍵字下降至合併節點 */
for j=x.n+1 to 2 /* child原有關鍵字後移一位 */
child.key(j) = child.key(j-1)
child.key(1) = x.key(i-1)
if !pLeft.leaf /* 移動左兄弟節點對應的孩子節點 */
for j=x.n+2 to 2 /* child原有孩子節點後移一位 */
child.c(j) = child.c(j-1)
child.c(1) = pLeft.c(x.n+1)
child.n = child.n+1
x.key(i) = pLeft.key(x.n)
pLeft.n = pLeft.n-1
else if pRight and pRight.n>=t /* case 3a: 將右兄弟節點的一個關鍵字挪到child節點 */
/* 父節點的第i個關鍵字下降到child節點 */
child.key(child.n+1) = x.key(i)
child.n = child.n+1
pRight.n = pRight.n-1
x.key(i) = pRight.key(1) /* pRight中最小關鍵字上升至x */
for j=1 to pRight.n
pRight.key(j) = pRight.key(j+1)
if !pRight.leaf
child.c(child.n+1) = pRight.c(1)
for j=1 to pRight.n+1
pRight.c(j) = pRight.c(j+1)
else if pLeft /* case 3b: 與左兄弟合併 */
B-TREE-MERGE-CHILD(x, i-1)
child = pLeft
else if pRight /* case 3b: 與右兄弟合併 */
B-TREE-MERGE-CHILD(x, i)
eB-TREE-DELETE-RECURSIVE(child, k) /* 關鍵字k不在節點x部分結束 */
三、搜索關鍵字
B樹搜索與二叉搜索樹相似,只是需要根據節點的孩子數做多路分支選擇。
B-TREE-SEARCH(x, k)
i = 1
while i<=x.n and k>x.key(i)
i = i+1
if i<=x.n and k==x.key(i)
return (x, i)
elseif x.leaf
return NIL
else DISK-READ(x, c(i))
return B-TREE-SEARCH(x.c(i), k)
總結
- B樹及其變種經常用於數據庫信息存儲,對於不同的應用場景可以使用不同的結構來存儲關鍵字,如鏈表,雙向隊列,數組等等。查找操作也可以使用順序查找、二分查找等實現。
- 在查找資料的時候發現explicit關鍵字,之前沒有用過,順帶mark一下。按默認規定,只用傳一個參數的構造函數也定義了一個隱式轉換。即可以通過如
A t = 'a';
,隱式傳入參數‘a’調用只有一個形參的構造函數A(char a);
。通過關鍵字explicit可以抑制隱式轉換,避免一些程序邏輯錯誤(這種錯誤可以通過編譯器編譯,難以發現)。