終於到最後一棵樹了,最後一顆樹是B樹,B樹是爲磁盤或其他直接存取的輔助設備而設計的一種平衡搜索樹,在降低磁盤I/O操作數方面要更好一些,許多數據庫系統使用B樹或者B樹的變種來存儲信息。
8.1 磁盤介紹
爲什麼介紹磁盤,因爲這個B樹就是爲了存儲在磁盤中使用的,介紹磁盤可以更好的理解B樹。
8.1.1 磁盤構造
計算機中有兩種存儲介質,一種是主存(main memory)通常由硅存儲芯片組成,就是內存;還有基於磁盤的輔存(secondary storage),就是硬盤之類的。內存容量一般都比較小,硬盤大小會比內存的大小多幾個數量級,如果我們數據量過大,還有需要斷電不丟失,就需要存儲在硬盤中。
這樣就很清楚了,驅動器由一個或多個盤片(platter)組成,它們以一個固定的速度繞着一個共同的主軸旋轉。每個盤的表面覆蓋着一層可磁化的物質。驅動器通過磁臂末尾的磁頭來讀/寫盤片。磁臂可以將磁頭向主軸移近或移遠。當一個給定的磁頭處於靜止時,它下面經過的磁盤表面稱爲一個磁道。多個盤片增加的僅僅是磁盤的容量,而不影響性能。 (出自《算法導論》)
-
磁道
什麼是磁道呢?每個盤片都在邏輯上有很多的同心圓,最外面的同心圓就是 0 磁道。我們將每個同心圓稱作磁道(注意,磁道只是邏輯結構,在盤面上並沒有真正的同心圓)。硬盤的磁道密度非常高,通常一面上就有上千個磁道。但是相鄰的磁道之間並不是緊挨着的,這是因爲磁化單元相隔太近會相互產生影響。 -
扇區
那扇區又是什麼呢?扇區其實是很形象的,大家都見過摺疊的紙扇吧,紙扇打開後是半圓形或扇形的,不過這個扇形是由每個扇骨組合形成的。在磁盤上每個同心圓是磁道,從圓心向外呈放射狀地產生分割線(扇骨),將每個磁道等分爲若干弧段,每個弧段就是一個扇區。每個扇區的大小是固定的,爲 4K。扇區也是磁盤的最小存儲單位。 -
柱面
柱面又是什麼呢?如果硬盤是由多個盤片組成的,每個盤面都被劃分爲數目相等的磁道,那麼所有盤片都會從外向內進行磁道編號,最外側的就是 0 磁道。具有相同編號的磁道會形成一個圓柱,這個圓柱就被稱作磁盤的柱面,如圖 所示
當磁盤驅動器執行讀/寫功能時。盤片裝在一個主軸上,並繞主軸高速旋轉,當磁道在讀/寫頭(又叫磁頭) 下通過時,就可以進行數據的讀 / 寫了。一般磁盤分爲固定頭盤(磁頭固定)和活動頭盤。固定頭盤的每一個磁道上都有獨立的磁頭,它是固定不動的,專門負責這一磁道上數據的讀/寫。
活動頭盤 (如上圖)的磁頭是可移動的。每一個盤面上只有一個磁頭(磁頭是雙向的,因此正反盤面都能讀寫)。它可以從該面的一個磁道移動到另一個磁道。所有磁頭都裝在同一個動臂上,因此不同盤面上的所有磁頭都是同時移動的(行動整齊劃一)。當盤片繞主軸旋轉的時候,磁頭與旋轉的盤片形成一個圓柱體。各個盤面上半徑相同的磁道組成了一個圓柱面,我們稱爲柱面 。因此,柱面的個數也就是盤面上的磁道數。
參考博客https://www.cnblogs.com/sunsky303/p/11497448.html
http://c.biancheng.net/view/879.html
https://blog.csdn.net/guozuofeng/article/details/90369471
8.1.2 讀寫效率
磁盤上數據必須用一個三維地址唯一標示:柱面號(磁道)、盤面號、塊號(磁道上的扇區)。
讀/寫磁盤上某一指定數據需要下面3個步驟:
(1) 首先移動臂根據柱面號使磁頭移動到所需要的柱面上,這一過程被稱爲定位或查找 。
(2) 所有磁頭都定位到所有盤面的指定磁道上(磁頭都是雙向的)。這時根據盤面號來確定指定盤面上的磁道。(不是很清楚)
(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結構。
8.2 B樹的定義
8.2.1 B樹的定義
一顆B樹T具有以下性質的有根樹(根爲T.root)
-
每個結點x有下面屬性:
a,x.n,當前存儲在結點x中的關鍵字個數。
b,x,n個關鍵字本身x.key1,x.key2,…。x.keyx.n,以非降序存放,使得x.key1≤x.key2≤…≤x.keyx.n
c,x.leaf,一個布爾值,如果x是葉結點,則爲TRUE;如果x爲內部結點,則爲FALSE。 -
每個內部結點x還包含x.n+1個指向其孩子的指針x.c1,x.c2,…,x.cx.n+1。葉結點沒有孩子,所以它們的ci屬性沒有定義。
-
關鍵字x.keyi對存儲在各子樹中的關鍵字範圍加以分割:如果ki爲任意一個存儲在以x.ci爲根的子樹中的關鍵字,那麼
ki≤x.key1≤k2≤x.key2≤…≤x.keyx.n≤kx.n+1 -
每個葉子結點具有相同深度,即樹的高度h.
-
每個結點所包含的關鍵字個數有上限和下限。用一個被稱爲B樹的最小度數的固定整數t≥2來表示這些界。
a,除了根結點以外的每個結點必須至少有t-1個關鍵字。因此,除了根結點以外的每個內部結點至少有t個孩子。如果樹非空,根結點至少有一個關鍵字。
b,每個結點至多可包含2t-1個關鍵字。因此,一個內部結點至多可有2t個孩子。當一個結點恰好有2t-1個關鍵字時,稱該節點是滿的。 (來自《算法導論》)
8.2.2 自己總結
算法導論寫的真複雜,本來不想寫的,但是還是需要一個專業的定義,然後我自己再來一個定義,上面的定義可以簡單的理解爲,一個結點x有n個關鍵字,關鍵字是按非降序存放的,並且在x結點中,還有n+1的指向孩子的結點,各個孩子結點也是按照非降序排列的,x結點可以是內部結點也可以是葉子結點,每個葉子節點都具有相同的深度。
下面是b樹的度比較重要,除了根結點以外,每個結點至少有t-1個關鍵字,並且至多有2t-1個關鍵字,如果達到2t-1個關鍵字就需要分裂。
講了這麼多文字,來個B樹的圖更直觀:
8.2.3 B樹的結構
typedef int Elemtype;
#define BTREE_ENTRY(name, type) \
struct name \
{ \
struct type **child; \
Elemtype *key; \
int leaf; \
int num; \
}
typedef struct bTree_node
{
Elemtype data; //結點數據
BTREE_ENTRY(, bTree_node) bst; //B樹結點信息
}_bTree_node;
typedef struct bTree
{
struct bTree_node *root; //指向根結點
int degree; //B樹的度數
}_bTree;
從B樹的結點也看出是按B樹的定義實現的,num爲這個結點的關鍵字個數,leaf標記這個結點是否葉子結點,ley就是這個結點的關鍵字,child二重指針就是指向孩子的指針。B樹的根結點,有一個指向B樹的根結點的指針,還有一個degree表示B的度,這個度決定着B樹的結點的關鍵字個數爲degree-1≤num≤2*degree-1。
8.3 B樹的其他函數
8.3.1 B樹搜索
B樹的搜索根紅黑樹也差不多的,不過差別是紅黑樹是二叉,B樹是多叉,這個多叉的選擇是根據關鍵字的大小去尋找各自的子樹,遞歸查詢;因爲B樹有多個結點,一個結點有n個子結點,所以返回值需要返回結點
typedef struct bTree_position
{
struct bTree_node *x; //b的結點
int i; //結點中的第幾個元素
}_bTree_position;
/**
* @brief B樹的搜索
* @param p 輸出參數
* @retval
*/
int bTree_search(struct bTree_node *node, Elemtype k, struct bTree_position *p)
{
int i = 0;
assert(p);
//循環判斷k在結點上是哪個位置
while(i < node->bst.num && k > node->bst.key[i]) {
i++;
}
if(i < node->bst.num && k == node->bst.key[i]) {
//返回結點的指針和結點的第幾個元素
p->x = node;
p->i = i;
} else if(node->bst.leaf) { //如果是葉子結點
p->x = NULL;
p->i = 0;
} else {
bTree_search(node->bst.child[i], k, p);
}
return 0;
}
① 感覺還是先寫插入的比較好,不過都寫搜索了,就寫把,因爲這是B樹,一個結點有幾個關鍵字,所以返回值組織成一個結構體,這個結果體裏面有x結點的指針和x的關鍵字的下標,暫且這樣寫吧。
② 搜索的時候,傳入結點node,然後遍歷這個結點的關鍵字,如果k大於關鍵字,關鍵字需要往後走,如果不大,就是我們感興趣部分。
③ 首先判斷是否等於關鍵字,如果等於關鍵字的話,就是找到的這個值,返回。
④ 如果沒找到,也分爲兩種情況,一種是當前結點是葉子結點,這種情況就是說明沒有找到關鍵字,所以返回空。
⑤ 另一種情況是內部結點,內部結點的意思就是還有孩子結點,所以需要遞歸調用,往孩子結點繼續尋找。
8.3.2 B樹遍歷
B樹的遍歷比較簡單,就是遞歸加循環,結點到結點之間利用遞歸,一個結點利用循環,因爲一個結點有幾個關鍵字,所以需要循環遍歷。
代碼:
/**
* @brief B樹遍歷,遞歸遍歷
* @param
* @retval
*/
static void btree_printf(struct bTree_node *node)
{
int i = 0;
if(node == NULL)
return ;
//遍歷當前結點
printf("keynum = %d is_leaf = %d\n", node->bst.num, node->bst.leaf);
for(i=0; i<node->bst.num; i++) {
printf("%c ", node->bst.key[i]);
}
printf("\n");
for(i=0; i<=node->bst.num; i++) {
btree_printf(node->bst.child[i]);
}
}
8.4 B樹的插入
8.4.1 創建結點
按照慣例,插入的時候都會先創建結點,這樣才符合我們的步驟
/**
* @brief B樹創建結點
* @param p 輸出參數
* @retval
*/
struct bTree_node *bTree_creat_node(struct bTree *T, Elemtype k, int leaf)
{
//申請一個結點
struct bTree_node *node = (struct bTree_node *)malloc(sizeof(struct bTree_node));
assert(node);
//填充數據
node->data = 0; //不知道data是怎麼用,以後分析一些具體使用B樹的實例應該就清楚了
node->bst.num = 0; //node結點個數
node->bst.leaf = leaf; //是否是葉子結點,1爲葉子節點,0爲非葉子節點
node->bst.key = (Elemtype *)calloc(1, (2*T->degree-1)*sizeof(Elemtype)); //2t-1個關鍵字
node->bst.child = (struct bTree_node **)calloc(1, (2*T->degree)*sizeof(struct bTree_node *));
return node;
}
B樹的結點比較複雜,因爲要申請2degree-1的關鍵字和2degree個指向孩子的指針
8.4.2 插入預熱
插入結點,還是按原來的步驟一個一個添加,這次我們添加26個英文字母,這個B樹我選度爲3,關鍵字個數爲2 * 3 - 1=5,孩子結點爲 2 * 3 = 6.
-
添加A,B,C,D, E
這個關鍵字個數最大是5,所以前面5個都是直接插入的 -
插入F
插入F的時候,判斷到根結點爲5的時候,開始分裂
分裂的步驟:先申請一個結點作爲頭結點,這個結點爲s,原來的根結點作爲y,從第一個結點開始分裂,就形成上圖所示的分裂。
然後繼續插入F,
這樣就符合要求,記住是先分裂再添加 -
插入G,H
-
插入I
遍歷到右邊結點的時候,發現是滿結點,分裂,這個分裂跟結點分裂不一樣,這個直接調用分裂函數即可
分裂的結點是右邊結點的第3個,調用分裂函數,就可以得出如圖所示的結果,然後插入I
-
插入J K
-
插入L
這次又到了滿結點,又進行分裂
繼續插入L
-
插入M N
-
插入O
分裂
插入
-
插入P Q
-
插入R
分裂
插入
- 插入 S
插入S結點,根結點是滿的,所以要分裂,這個很容易不小心出錯,我也是出錯了,之後再回來改正的
插入S
12.插入T
-
插入U V W
分裂
插入U V W
-
插入X Y Z
分裂
插入
至此26個字母的B樹就插入完成。
8.4.3 插入結點
通過上的插入順序,應該瞭解到B樹插入的過程了,插入過程是先判斷是否爲滿結點,如果是滿的結點,先需要分裂,這個分裂我們下節再細講,這裏就先調用一個空函數,表示已經分裂了,如果不是滿結點,就進行插入,但是這個分裂也分爲兩種情況,一種是根結點,一種是其他結點,寫代碼的時候需要注意
代碼:
/**
* @brief B樹插入結點
* @param 輸出參數
* @retval
*/
static int btree_insert_nonfull(struct bTree *T, struct bTree_node *node, Elemtype k)
{
int i = node->bst.num - 1;
//這個纔是真正的插入函數
if(node->bst.leaf) {
//如果是葉子結點,就可以插入了
//循環比較關鍵字,看插入哪個位置
while(i>=0 && k<node->bst.key[i]) {
node->bst.key[i+1] = node->bst.key[i];
//能到這一步插入了,就都不是滿結點,不需要考慮溢出問題
i--;
}
//以i爲分界,往後移動,留下i作爲新結點的位置
node->bst.key[i+1] = k;
node->bst.num++;
}else { //不是葉子結點
//循環比較關鍵字,看插入哪個位置
while(i>=0 && k<node->bst.key[i]) i--;
//判斷child[i]指向的子結點是否是滿的
if(node->bst.child[i+1]->bst.num == T->degree*2-1) {
//分裂
btree_split_child(T, node, i+1);
//分裂完成之後,再判斷一下新添加到x結點的i+1的值和k比較
if(k > node->bst.key[i+1]) i++;
}
btree_insert_nonfull(T, node->bst.child[i+1], k);
}
return 0;
}
/**
* @brief B樹插入結點
* @param 輸出參數
* @retval
*/
int bTree_insert(struct bTree *T, Elemtype k)
{
//插入結點的時候,要先判斷是否是滿結點,判斷滿結點也是分兩種情況,一種是根結點,一種是其他結點
//根結點爲滿結點的時候
if(T->root->bst.num == T->degree*2-1) {
//分裂根結點
//創建結點x
struct bTree_node *x = bTree_creat_node(T, 0, 0);
x->bst.child[0] = T->root;
T->root = x;
btree_split_child(T, x, 0);
int i = 0;
if(k > x->bst.key[0]) i++;
btree_insert_nonfull(T, x->bst.child[i], k);
} else {
btree_insert_nonfull(T, T->root, k);
}
return 0;
}
這個插入結點不是完整的代碼,還有分裂需要補上,下節補。
8.4.4 分裂結點
B樹中插入一個關鍵字要比二叉搜索樹中插入一個關鍵字複雜的多,不能想二叉搜索樹那樣,尋找到要插入的位置的時候,直接創建一個新的加點,然後插入;B樹的插入,是將一個新的關鍵字插入到一個已經存在的葉子結點上。由於不能插入到一個滿的葉子結點,所以引入了一個操作,叫分裂;分裂是指將一個滿的結點y(2degree-1個關鍵字)按其中間關鍵字 y.keyi分裂爲兩個各包含degree-1個關鍵字的結點,中間關鍵字被提升到y的父節點,以標識兩顆新樹的劃分點。但是如果y的父結點也是滿的,就必須在插入新的關鍵字之前就進行分裂。
- 分裂其他結點
其他結點的分裂簡單一點,沒有那麼難,需要知道分裂結點的父節點x,和要分裂結點指向的x的下標i,通過x的下標i獲取到要分裂的結點y, 然後申請一個新的結點z,然後把y中的一部分數據拷貝到z中,把y中的中間關鍵字提升到父節點x中,
/**
* @brief B樹分裂結點
* @param 要分裂的結點的父節點x,和指向要分裂結點的x的下標i
* @retval
*/
static int btree_split_child(struct bTree *T, struct bTree_node *x, int i)
{
int j = 0;
//獲取到要分裂的結點的指針
struct bTree_node *y = x->bst.child[i];
//創建一個新的結點
struct bTree_node *z = bTree_creat_node(T, 0, y->bst.leaf);
//拷貝y的一半關鍵字給z
for(j = 0; j<T->degree-1; j++)
{
z->bst.key[j] = y->bst.key[T->degree+j];
}
//判斷是否是是葉子結點,如果不是,拷貝指針
if(!y->bst.leaf)
{
for(j = 0; j<=T->degree-1; j++)
{
z->bst.child[j] = y->bst.child[T->degree+j];
}
}
//更新y,z的num
y->bst.num = T->degree-1;
z->bst.num = T->degree-1;
//移動x的結點,留下i的空位,然後插入
for(j=x->bst.num; j>=i; j--)
{
x->bst.key[j+1] = x->bst.key[j];
}
x->bst.key[i] = y->bst.key[T->degree-1];
//移動x的孩子結點,添加指針z的指針
for(j=x->bst.num-1; j>=i; j--)
{
x->bst.child[j+1] = x->bst.child[j];
}
x->bst.child[i+1] = z;
x->bst.num += 1;
return 0;
}
其他結點分裂,只要按照參數傳參即可,這個函數內部已經實現了
- 分裂根結點
分裂根結點不一樣的地方是要重新申請一個根結點做爲x,原來的根結點作爲y,x分裂的位置爲1,一些細節還是需要注意
代碼如下:
//分裂根結點
//創建結點x
struct bTree_node *x = bTree_creat_node(T, 0, 0);
x->bst.child[0] = T->root;
T->root = x;
btree_split_child(T, x, 0);
int i = 0;
if(k > x->bst.key[0]) i++;
btree_insert_nonfull(T, x->bst.child[i], k);
分裂根結點上面也有了,這裏在補補。
8.5 B樹的刪除
樹的刪除都是比較麻煩的,因爲刪除樹的結點的時候,需要考慮到全面,不像插入的時候,只考慮當前,但是有插入就有刪除,這是必須的,所以刪除的操作也必須要熟悉。
8.5.1 釋放結點
還是先從簡答的說起,有沒有發現,在其他樹刪除的時候,都沒有這個釋放結點,那是因爲其他樹的時候結點都這麼複雜,只要free一次就足夠了,但是B樹是多子樹,每一個結點中升申請了一塊內存存放關鍵字,還有申請了一部分空間存放孩子結點的指針,這樣都釋放結點的時候都應該得到釋放。
代碼:
/**
* @brief B樹釋放結點
* @param p 輸出參數
* @retval
*/
int bTree_destroy_node(struct bTree_node *node)
{
assert(node);
//釋放關鍵字內存
free(node->bst.key);
//釋放指向孩子結點指針內存
free(node->bst.child);
//釋放結點
free(node);
return 0;
}
很簡單,依次釋放內存
8.5.2 刪除預熱
刪除的時候,還是按照老辦法,一個一個刪除,感受一下刪除的情況。
- 刪除z
刪除z是最簡單的,也符合刪除的第一種情況:要刪除的元素在葉子節點,所以只需要直接刪除。這時候就有人眼尖看到了如果刪除A的話呢,A也是葉子節點,是不是直接刪除,這個刪除A的等下就講,先簡單然複雜。
2. 刪除U
刪除U是符合第2種情況,但是第二種情況也分爲了3種小情況,刪除U是符合2.2的情況,因爲後於U的孩子結點的關鍵字大於T->degree-1,所以按2.2處理。
3. 刪除O
刪除O符合2.3這種情況,左右兩邊的孩子的關鍵字都等於T->degee-1,所以需要用2.3的歸併再刪除的方式刪除。
歸併結果:
然後刪除O:
-
刪除A
這第3種情況算法導論說的我也不是很明白,不過通過程序倒推回來,還是可以理解的,如果理解不對的地方可以在評論中指出,我好改正。
我的理解是這樣的,刪除z的情況符合第1個條件,是葉子結點,並且關鍵字大於T->degree-1,但是如果要刪除A呢,這個也是葉子結點,明顯跟算法導論說的不太一樣,所以我在條件1加了一個條件,以便區別。
就以刪除A舉例,A的結點和右結點都只有T->degree-1個關鍵字,所以不能直接刪除,這時候就要利用在遞歸查找過程中,先判斷孩子結點的情況,先判斷孩子結點的關鍵字是否等於T->degree-1,如果不是,就獲得這個孩子結點的指針,然後遞歸調用。
如果是的話,就需要分類討論: -
如果這個孩子結點的兄弟結點的關鍵字大於T->degree個關鍵字,則將x中的某一個關鍵字將至孩子結點中,然後再從相鄰的結點中提取一個關鍵字,升到父節點中,這樣孩子結點的關鍵字就等於T->degree個了,可以繼續遞歸調用
-
如果這個孩子結點以及所有相鄰的兄弟結點都只包含T->degree-1個關鍵字,則將這個孩子結點和兄弟結點進行合併,然後在把父節點中的一個關鍵字移到新合併的結點中,這個新的關鍵字就是新結點的中間關鍵字。
寫了這麼多,不知道有沒有了解,不瞭解的研究下代碼,就會理解了,代碼的邏輯很清晰,不像描述成文字這麼難。
刪除A步驟:
剛開始遞歸,判斷I的左孩子是等於T->degree-1個關鍵字,符合3.1情況,開始移位,移位後結果:
進入CFI結點,繼續處理,這次左邊孩子結點不存在,右邊的孩子結點(DE)滿足等於T->degree-1個關鍵字,所以符合3.2情況
首先合併:
然後刪除:
刪除A完成。
8.5.3 刪除遇到的情況
-
如果關鍵字k在結點x中,並且x是葉結點,x的結點的關鍵字大於T->degree則從x中刪除k。
-
如果關鍵字k在結點x中,並且x是內部結點,則要判斷如下
2.1 x結點中前於k的子結點y,至少包含T->degree個關鍵字,則找出k在以y爲根的子樹中的前驅k’,遞歸的刪除k’,並在x中用k’代替k。
2.2 對稱地,如果y有小於T->degree個關鍵字,則檢查結點x中後於k的子結點z。如果z至少有T->degree個關鍵字,則找出k在以z爲根的子樹中後繼k’,遞歸的刪除k’,並在x中用k’替換k。
2.3 否則,如果y和z都只含有T->degree個關鍵字,則將k和z全部合併進y中,這樣x就失去了k和指向z的指針,並且y現在包含2T->degree-1個關鍵字,然後釋放z,並遞歸的地從y中刪除k。
3.孩子結點關鍵字等於T->degree-1的時候
3.1 如果這個孩子結點的兄弟結點的關鍵字大於T->degree個關鍵字,則將x中的某一個關鍵字將至孩子結點中,然後再從相鄰的結點中提取一個關鍵字,升到父節點中,這樣孩子結點的關鍵字就等於T->degree個了,可以繼續遞歸調用。
3.2 如果這個孩子結點以及所有相鄰的兄弟結點都只包含T->degree-1個關鍵字,則將這個孩子結點和兄弟結點進行合併,然後在把父節點中的一個關鍵字移到新合併的結點中,這個新的關鍵字就是新結點的中間關鍵字。
8.5.4 合併結點
B樹的刪除,有點借位的思想,如果不夠就往其他孩子結點借,如果左右孩子都不夠,就把左右孩子合併成一起,再刪除,這次說的就是合併。
代碼:
/**
* @brief 歸併結點
* @param
* @retval
*/
static int btree_merge(struct bTree *T, struct bTree_node *node, int idx)
{
//歸併的只要思路,就是把node->bst.child[idx] 和 node->bst.child[idx+1]歸併 ,然後插入node->key[idx]作爲中間關鍵字
int i=0;
struct bTree_node *left = node->bst.child[idx];
struct bTree_node *right = node->bst.child[idx+1];
//data merge
left->bst.key[T->degree-1] = node->bst.key[idx]; //node->key[idx]作爲中間關鍵字
for(i=0; i<T->degree-1; i++)
{
left->bst.key[T->degree+i] = right->bst.key[i];
}
if(!left->bst.leaf)
{
for(i=0; i<T->degree; i++)
{
left->bst.child[T->degree+i] = right->bst.child[i];
}
}
left->bst.num += T->degree; //還有一個key
bTree_destroy_node(right);
//node 刪除node[idx],從後往前移
for(i=idx+1; i<node->bst.num; i++)
{
//這個拷貝的時候需要很注意
node->bst.key[i-1] = node->bst.key[i];
node->bst.child[i] = node->bst.child[i+1];
}
node->bst.child[node->bst.num] = NULL;
node->bst.num -= 1;
if(node->bst.num == 0) {
T->root = left;
bTree_destroy_node(node);
}
return 0;
}
8.5.5 結點刪除
結點刪除就是按照8.5.3中那幾個情況進行處理的,代碼判斷邏輯比較清晰明瞭,好好看看就明白了。
代碼:
/**
* @brief B樹刪除結點,遞歸調用
* @param 輸出參數
* @retval
*/
static void btree_delete_key(struct bTree *T, struct bTree_node *node, Elemtype k)
{
int idx = 0, i;
if(node == NULL) return ;
//遍歷查找k是否在當前結點node中,這個是從0下標找起
while(idx<node->bst.num && k > node->bst.key[idx]) {
idx++;
}
printf("btree_delete_key %d %d %c %c\n", node->bst.num, node->bst.leaf, k, node->bst.key[idx]);
if(idx < node->bst.num && k == node->bst.key[idx]) {
//如果在當前結點上
if(node->bst.leaf) {
//判斷是否是葉子結點,如果是葉子節點就直接刪除
//符合第1種情況
printf("葉子點\n");
//刪除結點,把後面的結點往前移,這個是葉子結點,不用移動指針
for(i=idx; i<node->bst.num-1; i++) {
node->bst.key[i] = node->bst.key[i+1];
}
node->bst.key[node->bst.num-1] = 0;
node->bst.num -= 1;
if(node->bst.num == 0) { //如果num=0,說明只剩根結點了
bTree_destroy_node(node); //釋放根結點
T->root = NULL;
}
return ;
}
//後面的都不是葉子結點的
else if(node->bst.child[idx]->bst.num >= T->degree) { //前於k的子結點y,至少包含T->degree各個關鍵字
//找出k在以**y爲根的子樹中的前驅k',遞歸的刪除k'**,並在x中用k'代替k。
printf("前驅\n");
struct bTree_node *left = node->bst.child[idx];
//用k'替換k
node->bst.key[idx] = left->bst.key[node->bst.num-1];
//遞歸替換
btree_delete_key(T, left, left->bst.key[node->bst.num-1]);
}
else if(node->bst.child[idx+1]->bst.num >= T->degree) { //後於k的子結點z,至少包含T->degree各個關鍵字
//找出k在以**z爲根的子樹中的後驅k',遞歸的刪除k'**,並在x中用k'代替k。
printf("後驅\n");
struct bTree_node *right = node->bst.child[idx+1];
//用k'替換k
node->bst.key[idx] = right->bst.key[0];
//遞歸替換
btree_delete_key(T, right, right->bst.key[0]);
}
else { //y和z都包含T->degree-1個關鍵字,需要合併,然後在從y中遞歸刪除k
printf("其他\n");
btree_merge(T, node, idx); //合併
btree_delete_key(T, node->bst.child[idx], k); //遞歸刪除
}
} else {
//如果不在當前結點,就往孩子結點找
struct bTree_node *child = node->bst.child[idx];
if(child == NULL) return ;
if(child->bst.num == T->degree - 1) { //判斷孩子結點的個數是不是小於最小值,如果是就要特殊處理
struct bTree_node *left = NULL;
struct bTree_node *right = NULL;
if (idx - 1 >= 0)
left = node->bst.child[idx-1];
if (idx + 1 <= node->bst.num)
right = node->bst.child[idx+1];
if((left && left->bst.num >= T->degree) ||
(right && right->bst.num >= T->degree)) {
int richR = 0;
if(right) richR = 1;
if (left && right) richR = (right->bst.num > left->bst.num) ? 1 : 0;
printf("右孩子\n");
if(right && right->bst.num >= T->degree && richR) { //找到右邊兄弟替補
//先把node結點的數據往child裏放,放到child最後一個key中
child->bst.key[child->bst.num] = node->bst.key[idx];
//把right的第一個孩子指針掛接到child的最後一個指針上
child->bst.child[child->bst.num+1] = right->bst.child[0];
child->bst.num++;
//把右孩子結點往父節點提
node->bst.key[idx] = right->bst.key[0];
//右孩子結點往前移
for(i=0; i<right->bst.num-1; i++) {
right->bst.key[i] = right->bst.key[i+1];
right->bst.child[i] = right->bst.child[i+1];
}
right->bst.child[right->bst.num-1] = right->bst.child[right->bst.num];
right->bst.child[right->bst.num] = NULL;
right->bst.num--;
} else{ //找到左邊兄弟替補
//左邊跟右邊不是對稱關係,操作有點不一樣,不過大體都一樣的
printf("左孩子\n");
//先移動child的結點,空出第一個結點,等到父節點node插入元素
for(i=child->bst.num; i>0; i--) {
child->bst.key[i] = child->bst.key[i-1];
child->bst.child[i+1] = child->bst.child[i];
}
child->bst.child[1] = child->bst.child[0];
//把左邊孩子最後一個孩子指針掛接到child的0
child->bst.child[0] = left->bst.child[left->bst.num];
//把父結點的關鍵字賦值給left的0下標
child->bst.key[0] = node->bst.key[idx-1];
child->bst.num++;
//把左孩子的最後一個元素賦值給父節點的元素
node->bst.key[idx-1] = left->bst.key[left->bst.num];
left->bst.child[left->bst.num] = NULL;
left->bst.num--;
}
} else if((!left || left->bst.num == T->degree-1) && //左右兄弟都小於T->degree-1的時候
(!right || right->bst.num == T->degree-1)) {
if(left && left->bst.num == T->degree-1) { //左邊兄弟存在,並且滿足T->degree-1
btree_merge(T, node, idx-1);
child = left;
} else if(right && right->bst.num == T->degree-1) { //右邊兄弟存在,並且滿足T->degree-1
btree_merge(T, node, idx);
btree_printf(T->root);
printf("\n");
}
}
}
//如果不是,直接調用刪除函數
btree_delete_key(T, child, k);
}
}
這次B樹寫的跟實際運用,還是很大差別的,以後有時間研究一下實際工程中的應用,再寫一篇真正實用的,這次就相當預習了。