B樹的定義
假設B樹的度爲t(t>=2),則B樹滿足如下要求:(參考算法導論)
(1) 每個非根節點至少包含t-1個關鍵字,t個指向子節點的指針;至多包含2t-1個關鍵字,2t個指向子女的指針(葉子節點的子女爲空)。
(2) 節點的所有key按非降序存放,假設節點的關鍵字分別爲K[1], K[2] … K[n], 指向子女的指針分別爲P[1], P[2]…P[n+1],其中n爲節點關鍵字的個數。則有:
P[1] <= K[1] <= P[2] <= K[2] …..<= K[n] <= P[n+1] // 這裏P[n]也指其指向的關鍵字
(3) 若根節點非空,則根節點至少包含兩個子女;
(4) 所有的葉子節點都在同一層。
B樹的搜索,search(root, target)
從root出發,對每個節點,找到大於或等於target關鍵字中最小的K[i],如果K[i]與target相等,則查找成功;否則在P[i]中遞歸搜索target,直到到達葉子節點,如仍未找到則說明關鍵字不在B樹中,查找失敗。
B樹的插入,insert(root, target)
B樹的插入需要沿着搜索的路徑從root一直到葉節點,根據B樹的規則,每個節點的關鍵字個數在[t-1, 2t-1]之間,故當target要加入到某個葉子時,如果該葉子節點已經有2t-1個關鍵字,則再加入target就違反了B樹的定義,這時就需要對該葉子節點進行分裂,將葉子以中間節點爲界,分成兩個包含t-1個關鍵字的子節點,同時把中間節點提升到該葉子的父節點中,如果這樣使得父節點的關鍵字個數超過2t-1,則要繼續向上分裂,直到根節點,根節點的分裂會使得樹加高一層。
上面的過程需要回溯,那麼能否從根下降到葉節點後不回溯就能完成節點的插入呢?答案是肯定的,核心思想就是未雨綢繆,在下降的過程中,一旦遇到已滿的節點(關鍵字個數爲2t-1),就就對該節點進行分裂,這樣就保證在葉子節點需要分裂時,其父節點一定是非滿的,從而不需要再向上回溯。
B樹的刪除,delete(root, target)
在刪除B樹節點時,爲了避免回溯,當遇到需要合併的節點時就立即執行合併,B樹的刪除算法如下:從root向葉子節點按照search規律遍歷:
(1) 如果target在葉節點x中,則直接從x中刪除target,情況(2)和(3)會保證當再葉子節點找到target時,肯定能借節點或合併成功而不會引起父節點的關鍵字個數少於t-1。
(2) 如果target在分支節點x中:
(a) 如果x的左分支節點y至少包含t個關鍵字,則找出y的最右的關鍵字prev,並替換target,並在y中遞歸刪除prev。
(b) 如果x的右分支節點z至少包含t個關鍵字,則找出z的最左的關鍵字next,並替換target,並在z中遞歸刪除next。
(c) 否則,如果y和z都只有t-1個關鍵字,則將targe與z合併到y中,使得y有2t-1個關鍵字,再從y中遞歸刪除target。
(3) 如果關鍵字不在分支節點x中,則必然在x的某個分支節點p[i]中,如果p[i]節點只有t-1個關鍵字。
(a) 如果p[i-1]擁有至少t個關鍵字,則將x的某個關鍵字降至p[i]中,將p[i-1]的最大節點上升至x中。
(b) 如果p[i+1]擁有至少t個關鍵字,則將x個某個關鍵字降至p[i]中,將p[i+1]的最小關鍵字上升至x個。
(c) 如果p[i-1]與p[i+1]都擁有t-1個關鍵字,則將p[i]與其中一個兄弟合併,將x的一個關鍵字降至合併的節點中,成爲中間關鍵字。
B樹的實現
數據結構
-
/**
-
* @brief the degree of btree
-
* key per node: [M-1, 2M-1]
-
* child per node: [M, 2M]
-
*/
-
#define M 2 // M爲B樹的度
-
-
typedef struct btree_node {
-
int k[2*M-1];
-
struct btree_node *p[2*M];
-
int num;
-
bool is_leaf;
- } btree_node;
創建B樹
-
btree_node *btree_node_new()
-
{
-
btree_node *node = (btree_node *)malloc(sizeof(btree_node));
-
if(NULL == node) {
-
return NULL;
-
}
-
-
for(int i = 0; i < 2 * M -1; i++) {
// 初始化key
-
node->k[i] = 0;
-
}
-
-
for(int i = 0; i < 2 * M; i++) {
// 初始化pointer
-
node->p[i] = NULL;
-
}
-
-
node->num = 0;
-
node->is_leaf = true;
// 默認爲葉子
-
}
-
-
btree_node *btree_create()
-
{
-
btree_node *node = btree_node_new();
-
if(NULL == node) {
-
return NULL;
-
}
-
-
return node;
- }
插入節點
- // 當child滿時,將其進行分裂,child = parent->p[pos]
-
int btree_split_child(btree_node *parent, int pos, btree_node *child)
-
{
- // 創建新的節點
-
btree_node *new_child = btree_node_new();
-
if(NULL == new_child) {
-
return -1;
-
}
-
new_child->is_leaf = child->is_leaf;
-
new_child->num = M - 1;
-
- // 將child後半部分的key拷貝給新節點
-
for(int i = 0; i < M - 1; i++) {
-
new_child->k[i] = child->k[i+M];
-
}
-
if(false == new_child->is_leaf) {
-
for(int i = 0; i < M; i++) {
-
new_child->p[i] = child->p[i+M];
-
}
-
}
-
-
child->num = M - 1;
-
for(int i = parent->num; i > pos; i--) {
-
parent->p[i+1] = parent->p[i];
-
}
-
parent->p[pos+1] = new_child;
-
-
for(int i = parent->num - 1; i >= pos; i--) {
-
parent->k[i+1] = parent->k[i];
-
}
-
parent->k[pos] = child->k[M-1];
-
-
parent->num += 1;
-
}
-
void btree_insert_nonfull(btree_node *node, int target)
-
{
-
if(1 == node->is_leaf) {
// 如果在葉子中找到,直接刪除
-
int pos = node->num;
-
while(pos >= 1 && target < node->k[pos-1]) {
-
node->k[pos] = node->k[pos-1];
-
pos--;
-
}
-
-
node->k[pos] = target;
-
node->num += 1;
-
-
} else { // 沿着查找路徑下降
-
int pos = node->num;
-
while(pos > 0 && target < node->k[pos-1]) {
-
pos--;
-
}
-
-
if(2 * M -1 == node->p[pos]->num) {
-
btree_split_child(node, pos, node->p[pos]);
// 如果路徑上有滿節點則分裂
-
if(target > node->k[pos]) {
-
pos++;
-
}
-
}
-
-
btree_insert_nonfull(node->p[pos], target);
-
}
-
}
-
-
btree_node* btree_insert(btree_node *root, int target)
-
{
-
if(NULL == root) {
-
return NULL;
-
}
- // 對根節點的特殊處理,如果根是滿的,唯一使得樹增高的情形
- // 先申請一個新的
-
if(2 * M - 1 == root->num) {
-
btree_node *node = btree_node_new();
-
if(NULL == node) {
-
return root;
-
}
-
-
node->is_leaf = 0;
- node->p[0] = root;
-
btree_split_child(node, 0, root);
-
btree_insert_nonfull(node, target);
-
return node;
-
} else {
-
btree_insert_nonfull(root, target);
-
return root;
-
}
- }
刪除節點
- // 將y,root->k[pos], z合併到y節點,並釋放z節點,y,z各有M-1個節點
-
void btree_merge_child(btree_node *root, int pos, btree_node *y, btree_node *z)
-
{
- // 將z中節點拷貝到y的後半部分
-
y->num = 2 * M - 1;
-
for(int i = M; i < 2 * M - 1; i++) {
-
y->k[i] = z->k[i-M];
-
}
-
y->k[M-1] = root->k[pos];
// k[pos]下降爲y的中間節點
-
- // 如果z非葉子,需要拷貝pointer
-
if(false == z->is_leaf) {
-
for(int i = M; i < 2 * M; i++) {
-
y->p[i] = z->p[i-M];
-
}
-
}
-
for(int j = pos + 1; j < root->num; j++) {
-
root->k[j-1] = root->k[j];
-
root->p[j] = root->p[j+1];
-
}
-
-
root->num -= 1;
-
free(z);
-
}
-
btree_node *btree_delete(btree_node *root, int target)
-
{
- // 特殊處理,當根只有兩個子女,切兩個子女的關鍵字個數都爲M-1時,合併根與兩個子女
- // 這是唯一能降低樹高的情形
-
if(1 == root->num) {
-
btree_node *y = root->p[0];
-
btree_node *z = root->p[1];
-
if(NULL != y && NULL != z &&
-
M - 1 == y->num && M - 1 == z->num) {
-
btree_merge_child(root, 0, y, z);
-
free(root);
-
btree_delete_nonone(y, target);
-
return y;
-
} else {
-
btree_delete_nonone(root, target);
-
return root;
-
}
-
} else {
-
btree_delete_nonone(root, target);
-
return root;
-
}
-
}
-
void btree_delete_nonone(btree_node *root, int target)
-
{
-
if(true == root->is_leaf) {
// 如果在葉子節點,直接刪除
-
int i = 0;
-
while(i < root->num && target > root->k[i]) i++;
-
if(target == root->k[i]) {
-
for(int j = i + 1; j < 2 * M - 1; j++) {
-
root->k[j-1] = root->k[j];
-
}
-
root->num -= 1;
-
} else {
-
printf("target not found\n");
-
}
-
} else { // 在分支中
-
int i = 0;
-
btree_node *y = NULL, *z = NULL;
-
while(i < root->num && target > root->k[i]) i++;
-
if(i < root->num && target == root->k[i]) {
// 如果在分支節點找到target
-
y = root->p[i];
-
z = root->p[i+1];
- if(y->num > M - 1) {
-
// 如果左分支關鍵字多於M-1,則找到左分支的最右節點prev,替換target
- // 並在左分支中遞歸刪除prev,情況2(a)
-
int pre = btree_search_predecessor(y);
-
root->k[i] = pre;
-
btree_delete_nonone(y, pre);
- } else if(z->num > M - 1) {
-
// 如果右分支關鍵字多於M-1,則找到右分支的最左節點next,替換target
- // 並在右分支中遞歸刪除next,情況2(b)
-
int next = btree_search_successor(z);
-
root->k[i] = next;
-
btree_delete_nonone(z, next);
-
} else {
- // 兩個分支節點數都爲M-1,則合併至y,並在y中遞歸刪除target,情況2(c)
-
btree_merge_child(root, i, y, z);
-
btree_delete(y, target);
-
}
-
} else { // 在分支沒有找到,肯定在分支的子節點中
-
y = root->p[i];
-
if(i < root->num) {
-
z = root->p[i+1];
-
}
-
btree_node *p = NULL;
-
if(i > 0) {
-
p = root->p[i-1];
-
}
-
-
if(y->num == M - 1) {
-
if(i > 0 && p->num > M - 1) {
- // 左鄰接節點關鍵字個數大於M-1
-
btree_shift_to_right_child(root, i-1, p, y);
//情況3(a)
-
} else if(i < root->num && z->num > M - 1) {
- // 右鄰接節點關鍵字個數大於M-1
-
btree_shift_to_left_child(root, i, y, z);
// 情況3(b)
-
} else if(i > 0) {
-
btree_merge_child(root, i-1, p, y); // 情況3(c)
-
y = p;
-
} else {
-
btree_merge_child(root, i, y, z);
// 情況3(c)
-
}
-
btree_delete_nonone(y, target);
-
} else {
-
btree_delete_nonone(y, target);
-
}
-
}
-
-
}
-
}
-
-
int btree_search_predecessor(btree_node *root)
-
{
-
btree_node *y = root;
-
while(false == y->is_leaf) {
-
y = y->p[y->num];
-
}
-
return y->k[y->num-1];
-
}
-
-
int btree_search_successor(btree_node *root)
-
{
-
btree_node *z = root;
-
while(false == z->is_leaf) {
-
z = z->p[0];
-
}
-
return z->k[0];
-
}
-
-
void btree_shift_to_right_child(btree_node *root, int pos,
-
btree_node *y, btree_node *z)
-
{
-
z->num += 1;
-
for(int i = z->num -1; i > 0; i--) {
-
z->k[i] = z->k[i-1];
-
}
-
z->k[0]= root->k[pos];
-
root->k[pos] = y->k[y->num-1];
-
-
if(false == z->is_leaf) {
-
for(int i = z->num; i > 0; i--) {
-
z->p[i] = z->p[i-1];
-
}
-
z->p[0] = y->p[y->num];
-
}
-
-
y->num -= 1;
-
}
-
void btree_shift_to_left_child(btree_node *root, int pos,
-
btree_node *y, btree_node *z)
-
{
-
y->num += 1;
-
y->k[y->num-1] = root->k[pos];
-
root->k[pos] = z->k[0];
-
-
for(int j = 1; j < z->num; j++) {
-
z->k[j-1] = z->k[j];
-
}
-
-
if(false == z->is_leaf) {
-
y->p[y->num] = z->p[0];
-
for(int j = 1; j <= z->num; j++) {
-
z->p[j-1] = z->p[j];
-
}
-
}
-
-
z->num -= 1;
- }
插入與刪除過程(圖片爲層序遍歷的結果)
插入序列[18, 31, 12, 10, 15, 48, 45, 47, 50, 52, 23, 30, 20]
刪除序列[15, 18, 23, 30, 31, 52, 50, 48, 47, 45, 20, 12, 10]
B+樹
與B樹不同的時,B+樹的關鍵字都存儲在葉子節點,分支節點均爲索引,在實現上大致與B樹類似,在幾個細節稍有不同。
(1) 數據結構中增加prev,next指針,用於將葉子節點串成有序雙向鏈表。
(2) 在節點分裂的時候,如果分裂的節點爲葉子,則需要把中間節點保留在左(或右)邊的分支上,並且需要更新prev和next。
(3) 在節點合的時候,如果合併的節點爲葉子,不需要把跟節點下降爲中間節點,並且需要更新prev和next。
(4) 在向鄰接節點借節點時,借來的關鍵字並不是父節點的關鍵字,而是鄰接點的關鍵字,並根據實際情況更新父節點的索引。
本文爲轉載,原文鏈接爲:http://blog.chinaunix.net/uid-20196318-id-3030529.html