http://blog.chinaunix.net/uid-20196318-id-3030529.html
B樹的定義
假設B樹的度爲t(t>=2),則B樹滿足如下要求:(參考算法導論)
每個非根節點至少包含t-1個關鍵字,t個指向子節點的指針;至多包含2t-1個關鍵字,2t個指向子女的指針(葉子節點的子女爲空)。
即:[t-1,2*t-1]
(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樹的刪除操作:
對於一顆度爲n的B樹,它的關鍵字刪除操作,需要考慮刪除關鍵字後,節點是否還能繼續保持B樹的性質,如果節點擁有n-1個關鍵字,那麼直接刪除關鍵字的話會導致破壞B樹節點關鍵字最小數目的限制。如果對於非葉子節點直接刪除關鍵字,那麼會影響子節點指針數組的組成,破壞了子節點指針結構。所以來說最好的辦法就是把刪除的關鍵字設法移動到葉子節點中去刪除,因爲葉子節點沒有子節點指針數組,並且這個葉子節點的關鍵字數目要求至少是n,這樣才能保證成功的刪除關鍵字。
刪除關鍵字的算法借鑑了http://blog.chinaunix.net/uid-20196318-id-3030529.html,自己重新實現了一下,自己的代碼很醜。
Blog中描述的B樹刪除關鍵字的算法:(以下假設樹的度是3)
1.刪除的關鍵字在葉子節點中,直接在葉子節點中刪除關鍵字,後面的兩部會保證該葉子節點關鍵字的個數至少爲n個。
2.如果刪除的關鍵字在非葉子節點x中,x的數組下標爲i
a.如果x的左孩子節點存在,x->child[i],並且x->child[i]的節點關鍵字個數至少是n個,則找到 child[i]中最大的關鍵字max替換x中刪除的關鍵字,繼續遞歸child[i]刪除關鍵字max。
說明:左側節點紅色代表3是遞歸刪除的新的關鍵字
b.如果x的右孩子節點存在,x->child[i+1],並且x->child[i+1]的節點關鍵字個數至少是n個,則找到 child[i+1]中最小的關鍵字min替換x中刪除的關鍵字,繼續遞歸child[i+1]刪除關鍵字min。
c.如果x的左孩子和右孩子關鍵字數目都是n-1,那麼這個時候應該把這個關鍵字和自己的左右孩子節點做合併操作(反向的分裂節點操作),繼續遞歸刪除合併節點中的關鍵字。
3.如果刪除的關鍵字不再非葉子節點x中,在x節點的子節點的分支中child[i]中,且child[i]的關鍵字個數是n-
a.如果child[i-1]存在,並且關鍵字數目至少是n,那麼從child[i-1]選出最大的關鍵字,替換x中關鍵字,同時這個x中的老的關鍵字插入到child[i]中第一個,這時child[i-1]中由於最大的關鍵字被移動到了父節點,這是它的子節點指針會多一個,這時從child[i-1]中刪除它,正好放到child[i]子節點指針數組的第一個位置,來適應child[i]中新加入的父節點的關鍵字。
這個過程比較複雜,文字不好理解,如圖:
說明:上圖中,假設可以確定要刪除的關鍵字在5,6分支中,那麼這個分支的關鍵字的數量是2個(假設樹的度是3),那麼這個情況就不能保證case1中,如果是葉子節點直接刪除關鍵字的說法了,所以這個時候要保證5,6分支中的關鍵字從別的兄弟節點中借一個。默認先從左兄弟開始借(也可以從右兄弟開始借,不成功再左兄弟)。正確的借關鍵字的做法就是從左(1,2,3節點)兄弟中踢掉最右側的關鍵字和最右側的子節點指針,把這個關鍵字(圖中是3)放到父節點的4的位置上,同時把4節點放到5,6分支中,並且那個被左兄弟踢掉的子節點指針過渡到了5,6分支的第一個子節點指針的位置上。上圖中紅色的部分代表被移動的數據結構。
b.如果上一步失敗了(左兄弟的關鍵字個數不夠n個,或者左兄弟節點根本不存在),那麼就從child[i+1]中刪除第一個關鍵字和第一個子節點指針,關鍵字替代父節點的關鍵字,child[i]中在最後增加父節點的關鍵字和child[i+1]的子節點指針。類似第一步
c.如果上一步也失敗了(表示從左右兄弟不能借到關鍵字),這時就需要child[i]和child[i-1]或者是child[i+1](child[i-1],child[i+1]至少有個子節點存在)合併到一起(如果左右兄弟節點都存在的話,合併的順序可以隨意,但是如果其中一個兄弟節點爲空,爲空是因爲關鍵字是節點最後一個,或者是第一個關鍵字,這樣就只能合併那個存在的兄弟節點),這個過程就是B樹插入關鍵字的逆操作,然後在遞歸刪除這個合併節點中的關鍵字。
說明:上圖中的情況,查找關鍵字在4,5分支中,但是左右兄弟都是n-1個關鍵字,這是兄弟把4,5兄弟節點1,2(左兄弟合併到4,5節點上)。
下面是代碼:
#include <iostream>
#define M 6
//b樹的數據結構
typedef structbtree_node
{
int k[2*M-1];//每個節點最多包含M-1個節點最少包涵M-1個節點
struct btree_node *p[2*M];//每層最多包含*M個指針,最少M個指針
int num;
bool is_leaf;
}btree_node;
//創建B樹
void btree_delete_nonone(btree_node* root,int target);
btree_node* btree_node_new()
{
btree_node*node=(btree_node*)malloc(sizeof(btree_node));
if(NULL==node)
return NULL;
for(inti=0;i<2*M-1;i++)
node->k[i]=0;
for(inti=0;i<2*M;i++)
node->p[i]=NULL;
node->num=0;
node->is_leaf=true;//默認是葉子節點
return node;
}
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*newchild=btree_node_new();
if(NULL==newchild)
return -1;
newchild->is_leaf=child->is_leaf;
newchild->num=M-1;
//將child的後半部分拷貝給新節點
for(inti=0;i<M-1;i++)
newchild->k[i]=child->k[i+M];
//如果child不是葉子及誒單,還需要把指針拷貝過去,指針比節點多
if(false==newchild->is_leaf)
{
for(inti=0;i<M;i++)
newchild->p[i]=child->p[i+M];
}
child->num=M-1;
//child的中間節點需要插入parent的pos處,更新parent的key和pointer
for(inti=parent->num;i>pos;i--)
parent->p[i+1]=parent->p[i];
parent->p[pos+1]=newchild;
for(inti=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)
{//若p的某個子節點滿了
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 NULL;
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;
}
}
void levelOrderTraverse(btree_node* root)
{
btree_node*q[100];
int tail=1;
int head=0;
q[head]=root;
while(head<tail)
{
btree_node*temp=q[head++];
printf("[");
for(inti=0;i<temp->num;i++)
printf("%d ",temp->k[i]);
printf("] ");
for(inti=0;i<=temp->num;i++)
if(NULL!=temp->p[i])
q[tail++]=temp->p[i];
}
printf("\n");
}
void inOrderTraverse(btree_node* root)
{
if(root!=NULL)
{
inOrderTraverse(root->p[0]);
for(inti=0;i<root->num;i++)
{
printf("%d ",root->k[i]);
inOrderTraverse(root->p[i+1]);
}
}
}
void preOrderTraverse(btree_node* root)
{
if(NULL!=root)
{
for(inti=0;i<root->num;i++)
{
printf("%d ",root->k[i]);
}
for(inti=0;i<=root->num;i++)
{
preOrderTraverse(root->p[i]);
}
}
}
void postOrderTraverse(btree_node* root)
{
if(NULL!=root)
{
for(inti=0;i<=root->num;i++)
postOrderTraverse(root->p[i]);
for(inti=0;i<root->num;i++)
printf("%d ",root->k[i]);
}
}
//尋找rightmost,以root爲根的最大關鍵字
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];
}
//尋找以root爲根的最小關鍵字
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(inti=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(inti=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(intj=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(intj=1;j<=z->num;j++)
z->p[j-1]=z->p[j];
}
z->num-=1;
}
//將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)
{
y->num=2*M-1;
for(inti=M;i<2*M-1;i++)
y->k[i]=z->k[i-M];
y->k[M-1]=root->k[pos];
if(false==z->is_leaf)
for(inti=M;i<2*M;i++)
y->p[i]=z->p[i-M];
for(intj=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);//合併y和z和root
free(root);
btree_delete_nonone(y,target);
return y;
}
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(intj=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
int pre=btree_search_predecessor(y);
root->k[i]=pre;
btree_delete_nonone(y,pre);
}
else if(z->num>M-1)
{//如果右分支關鍵字多餘M-1
int next=btree_search_successor(z);
root->k[i]=next;
btree_delete_nonone(z,next);
}
else
{
//兩個分支都正好爲M-1都不能刪除
//將兩個分支與y和z與root[i]合併
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);
}
elseif(i<root->num&&z->num>M-1)
{
//有臨界點關鍵字個數大於M-1
btree_shift_to_left_child(root,i,y,z);
}
elseif(i>0)
{
//需要合併
btree_merge_child(root,i-1,p,y);
y=p;
}
else
{//
btree_merge_child(root,i,y,z);
}
btree_delete_nonone(y,target);
}
else
{
btree_delete_nonone(y,target);
}
}
}
}
#include <time.h>
int main()
{
int arr[] = {18, 31, 12, 10, 15, 48, 45, 47, 50, 52, 23, 30,20};
btree_node *root = btree_create();
for(inti = 0; i <sizeof(arr) / sizeof(int); i++) {
root = btree_insert(root, arr[i]);
}
levelOrderTraverse(root);
inOrderTraverse(root);
printf("\n");
preOrderTraverse(root);
printf("\n");
postOrderTraverse(root);
printf("\n");
int todel[] = {52};
for(int i = 0; i <sizeof(todel) /sizeof(int); i++) {
printf("after delete %d\n", todel[i]);
root= btree_delete(root, todel[i]);
levelOrderTraverse(root);
inOrderTraverse(root);
printf("\n");
preOrderTraverse(root);
printf("\n");
postOrderTraverse(root);
printf("\n");
}
return 0;
}
截圖: M=6