B樹的原理分析及實現
B樹是為磁碟或其他直接存取的輔助裝置而設計的一種平衡搜尋樹。許多資料庫系統使用B樹或B樹的變種來儲存資訊。為何會採用這種樹結構進行設計呢,《演算法導論》18章講得很到位,值得細細品味。
下面直接開始瞭解B樹的實現細節吧!
一、B樹的定義
它與二叉搜尋樹和紅黑樹一樣,把衛星資料和關鍵字都存放在同一個結點中。
1、每個結點的屬性:
(1)x.n: 每個結點包含的關鍵字個數;
(2)x.key1, x.key2, x.key3.....x.keyn, 以非降序排列。
(3)x.leaf, 表示x結點是否是葉子結點。
(4)x.ci: 每個結點還包含x.n+1個孩子指標x.c1, x.c2, x.c3......x.cn+1
2、x中的關鍵字對儲存在各子樹中的關鍵字範圍加以分割。
3、每個葉結點具有相同的深度,為樹的高度
4、我們可以為每棵B樹規定一個最小度數:T,那麼每個結點所包含的關鍵字個數的範圍:T-1 ~2T-1, 所包含的孩子結點個數為T ~ 2T。當結點關鍵字個數為2T - 1時,該結點滿。(根結點至少包含1個關鍵數,而其他結點至少包含T - 1個關鍵字。
5.B樹的高度:h<=logt (n+1)/2。由來演算法導論上很清晰地給出了證明了,容易推導而得。
二、B樹上的基本操作
1、B_Search
B樹在每個結點做的是一個多路的分支選擇。對於每個內部結點x, 做的是一個(x.n+1)路的分支選擇。
B_Tree_Search(x , k) : x為指向子樹根結點的指標,從該子樹中搜索 k, 返回k 所在結點 及其在結點中的索引(y,i)
i =1
while i<=x.n and k> x.keyi
i++
if i<=x.n and k = keyi
return (x,i)
if x.leaf
return NULL
else
Disk_Read(x,ci) //讀取x的第i個孩子
return B_Tree_Search(x.ci, k)
2、B_Insert
區別於二叉搜尋樹的插入,並非簡單地建立一個新的葉結點,然後將其插入。而是將新的關鍵字插入到一個已經存在的葉結點上。由於不能將關鍵字插入到一個滿的葉結點中,故引入一個操作,將一個滿的結點y,按照其中間關鍵字y.keyt分裂為各有t-1個關鍵字的結點, 而y.keyt則被提升到其父結點中。
3、B_Delete
B樹中,刪除某個關鍵字後必須保證該結點的關鍵字個數>=T-1.。因此一個簡單的刪除演算法,當要刪除關鍵字的路徑上結點有最少的關鍵字數時,類似於插入演算法,也可能要向上回溯。
分析刪除過程:
(1)、如果關鍵字k在結點x中, 並且x是葉結點,那麼從x中刪除k。
(2)、如果關鍵字k在結點x中,並且x是內部結點,那麼:
a、如果x中小於k 的孩子結點y,至少包含t個關鍵字,那麼找出k在以y為根的子樹中的前驅k',將該值替換掉k,並遞迴地刪除k'
b、如果y有小於t(即 t-1)個關鍵字,但是x中大於k的孩子結點z中,至少包含t個關鍵字,那麼找出k在以z為根的子樹中的後繼k',將該值替換掉k,並遞迴地刪除k'
c、如果y和z均只有t-1個關鍵字,那麼合併y和z,並將k作為它們的中間關鍵字,同時從x中刪除k, 釋放掉z後,從y中遞迴地刪除k。
(3)、如果關鍵字不在結點x中。那麼確定x.ci, 以它為根的子樹要包含k。如果x.ci只有t-1個關鍵字,必須執行一些操作,以保證它降至一個至少包含t個關鍵字的結點。然後,通過對x的某個子結點進行遞迴而結束:
a、如果x.ci中只包含t-1個關鍵字,而它的左相鄰的兄弟x.ci-1至少包含t個關鍵字,那麼,將x中的x.keyi-1降至x.ci中,將x.ci-1中的最大關鍵字升至x中。對稱去處理x.ci的右相鄰結點。
b、如果x.ci的左右相鄰結點均只包含t-1個關鍵字,那麼將x.ci與一個兄弟合併,將x的一個恰當的關鍵字移至新的結點中,使之成為它們的中間關鍵字。
具體實現程式碼:
// B_Tree.cpp : 定義控制檯應用程式的入口點。
//
#include "stdafx.h"
#include <stdio.h>
#include <iostream>
using namespace std;
#define T 4
typedef struct B_Tree_Node
{
int n; //指示該結點的關鍵字個數
int *keys; //該結點關鍵字陣列
bool isLeaf; //該結點是否是葉子結點
struct B_Tree_Node **child ; //該結點的所有孩子結點
struct B_Tree_Node *p; //該結點的父結點
}B_Tree_Node, *p_B_Tree_Node;
B_Tree_Node *alloact_Node()
{
B_Tree_Node *newNode = new B_Tree_Node;
newNode->n = 0;
newNode->isLeaf = true;
newNode->keys = new int[2*T-1];
newNode->child = new p_B_Tree_Node[2*T];
newNode->p = NULL;
for(int i=0;i<2*T;i++)
newNode->child[i] = NULL;
return newNode;
}
//從以當前結點為根結點的子樹中,尋找k所在的結點,
/*
curNode 表示該當前子樹的根結點,k 是要查詢的關鍵字, index用來儲存k在該結點中的索引
首先在當前結點中查詢 ,若在該結點中,則直接返回該結點指標;如果沒有找到,而且該結點是葉子結點,則返回NULL
否則,在它的適當的孩子結點中查詢
*/
B_Tree_Node * searchNode(B_Tree_Node *curNode, int k, int &index)
{
int i = 0;
while(i<=curNode->n && k >curNode->keys[i])
i++;
if(i<curNode->n && k == curNode->keys[i]) //找到了k
{
index = i;
return curNode;
}
if(curNode->isLeaf) //如果該結點是葉子結點,則k不存在
return NULL;
searchNode(curNode->child[i],k,index);
}
//splitNode_p是被分裂結點的父結點,i是索引為i的孩子為滿,需要被分裂
//被分裂的結點是滿的,那麼它的n = 2*T - 1;被分裂為兩個T-1個關鍵字的子結點,同時它的中間元素被提升到其父節點中
void BTree_Child_Split(B_Tree_Node *splitNode_p, int index_child)
{
B_Tree_Node *newChild = alloact_Node();
newChild->n = T-1;
for(int i = 0;i<T-1;i++)
{
newChild->keys[i] = splitNode_p->child[index_child]->keys[T+i];
}
splitNode_p->child[index_child]->n = T-1;
if(splitNode_p->child[index_child]->isLeaf!=true) //如果它的第i個孩子不是葉子結點,則將它的後T個孩子也送給newChild
{
newChild->isLeaf = false;
for(int i=0;i<T-1;i++)
newChild->child[i] = splitNode_p->child[i+T];
}
//將newChild 新增為splitNode_p的第i+1個孩子結點,將中間關鍵字提升到它中
for(int i = splitNode_p->n; i>=index_child;i--)
{
splitNode_p->child[i+1] = splitNode_p->child[i];
}
splitNode_p->n++;
splitNode_p->child[index_child+1] = newChild;
for(int i = splitNode_p->n-1; i>=index_child;i--)
{
splitNode_p->keys[i+1] = splitNode_p->keys[i];
}
splitNode_p->keys[index_child] = splitNode_p->child[index_child]->keys[T-1];
}
void BTree_Insert_NonFull(B_Tree_Node *nonfull, int k)
{
int i = nonfull->n - 1;
if(nonfull->isLeaf)
{
while(i>=0&&k<nonfull->keys[i])
{
nonfull->keys[i+1] = nonfull->keys[i];
i--;
}
i = i+1;
(nonfull->n)++;
nonfull->keys[i] = k;
}
else
{
while(i>=0&&k<nonfull->keys[i])
i--;
i = i+1;
if(nonfull->child[i]->n == 2*T-1)
{
BTree_Child_Split(nonfull,i);
if(k>nonfull->keys[i])
i = i+1;
}
BTree_Insert_NonFull(nonfull->child[i],k);
}
}
//在B_Tree中加入新的關鍵字,主要由BTree_Insert_NonFull來實現,確保每次插入時所訪問的結點都是非滿結點;
//首先,若根結點為滿,則分裂根結點
void BTree_Insert_Node(p_B_Tree_Node *root,int k)
{
B_Tree_Node *p = *root;
if(p->n == 2*T - 1) //如果根結點滿
{
B_Tree_Node *newRoot =alloact_Node();
newRoot->child[0] = (*root);
newRoot->isLeaf = false;
*root = newRoot;
BTree_Child_Split(newRoot,0);
BTree_Insert_NonFull(newRoot,k);
}
else
BTree_Insert_NonFull(*root,k);
}
void printBFS(B_Tree_Node *t)
{
if(NULL == t)
return;
//輸出當前節點所有關鍵字
cout << "[";
for(int i = 0;i < t->n;++i)
{
cout << t->keys[i];
if(t->n - 1 != i)
cout << " ";
}
cout << "]" << endl;
//遞迴輸出所有子樹
for(int i = 0;i <= t->n;++i)
printBFS(t->child[i]);
}
//delete
/*
subNode,當前結點,以它為根的樹中刪除k
1.看k是否存在於以subNode為根的子樹中,不存在則返回;存在則繼續
2.看k是否存在於當前結點:(1)存在且該節點為葉子節點,則直接刪除k;(2)存在且該節點為內部節點,分情況討論;(3)不存在,則找出以它的孩子x.ci為根的子樹中包含k,
Different Case:
1.存在於當前結點,且該結點為內部結點:
case 1:該結點中前於k的子結點y中有至少包含T個元素,則找出k'替換k,並遞迴地刪除k';
case 2:該結點中前於k的子結點z中有隻包含T-1個元素,但大於k的子結點中有至少包含T個元素,同上找出k'替換k,遞迴地刪除k';
case 3:以上兩個子結點均只包含T-1個元素,那麼將k與z子結點中的元素均歸併到y中。再遞迴刪除k
2.k不存在於當前的結點中,存在於以它的孩子x.ci為根的子樹中。
case 1:若x.ci中至少包含有T個元素,遞迴找刪除k
case 2:若x.ci中只有T-1個元素,而x.ci-1或x.ci+1中至少有T個元素,剛將x中合適的元素取出來給x.ci,x.ci-1或x.ci+1中合適的元素取出來給x,遞迴刪除k
case 3:若x.ci-1和x.ci+1中均只有T-1個元素,那麼將x.ci-1與x.ci合併,或另外兩個合併,並將x中合適的元素作為它們的中間關鍵字。
*/
void BTree_delete_key(B_Tree_Node *subNode, int k)
{
int index = 0;
B_Tree_Node *deleteNode = NULL;
if((deleteNode = searchNode(subNode,k,index)) == NULL)
return;
int keyIndex = -1;
for(int i=0;i<subNode->n;i++)
{
if(k == subNode->keys[i])
{
keyIndex = i;
break;
}
}
//如果在當前結點,且當前結點為葉子結點,則直接刪除k
//OK******************************
if(keyIndex != -1 && subNode->isLeaf)
{
for(int i=keyIndex;i<subNode->n-1;i++)
{
subNode->keys[i] = subNode->keys[i+1];
}
(subNode->n)--;
}
//如果在當前結點中,且當前結點不為葉子結點
else if(keyIndex != -1 && subNode->isLeaf!= true)
{
B_Tree_Node *processorNode = subNode->child[keyIndex];
B_Tree_Node *succssorNode = subNode->child[keyIndex+1];
//如果小於k的孩子結點關鍵字數大於T
if(processorNode->n >= T)
{
int k1 = processorNode->keys[processorNode->n-1];
subNode->keys[keyIndex] = k1;
BTree_delete_key(processorNode,k1);
}
//如果大於k的孩子結點關鍵字數大於T
else if(succssorNode->n >=T)
{
int k1 = succssorNode->keys[0];
subNode->keys[keyIndex] = k1;
BTree_delete_key(succssorNode,k1);
}
//如果兩個孩子結點關鍵字數均不大於T,則將k與右孩子結點的關鍵字歸併到左孩子中
else
{
for(int j=0;j<T-1;j++)
{
//processorNode->keys[T-1] = k;這裡最好不要使用T表示,因為如果是根結點的話,可能它的關鍵字數不為T
processorNode->keys[processorNode->n] = k;
processorNode->keys[processorNode->n+1+j] = succssorNode->keys[j];
}
processorNode->n = 2*T -1 ;
//將subNode->child[keyIndex+1]的孩子傳給左鄰孩子
if(!processorNode->isLeaf)
{
for(int j=0;j<T;j++)
{
processorNode->child[T+j] = succssorNode->child[j];
}
}
//修改subNode中的key值
for(int j = keyIndex;j<subNode->n-1;j++)
{
subNode->keys[j] = subNode->keys[j+1];
}
subNode->n = subNode->n - 1;
delete succssorNode;
BTree_delete_key(processorNode,k);
}
}
else if(keyIndex == -1) //不在當前結點中
{
int childIndex = 0;
B_Tree_Node *deleteNode = NULL;
//尋找合適的子孩子,以該子孩子為根的樹包含k
for(int j = 0;j<subNode->n;j++)
{
if(k<subNode->keys[j])
{
childIndex = j;
deleteNode = subNode->child[j];
break;
}
}
//如果該子孩子的關鍵字數小於T,考慮那兩種情況
if(deleteNode->n <= T-1)
{
//deleteNode的左兄弟結點
B_Tree_Node *LeftNode = subNode->child[childIndex-1];
//deleteNode的右兄弟結點
B_Tree_Node *RightNode = subNode->child[childIndex+1];
//如果左兄弟結點關鍵字數大於T,將父結點中的第childIndex-1個元素送給deleteNode,將Left中的最大元素送給父結點,
if(childIndex>=1 && LeftNode->n >= T)
{
for(int i = deleteNode->n;i>0;i--)
{
deleteNode->keys[i] = deleteNode->keys[i-1];
}
deleteNode->keys[0] = subNode->keys[childIndex];
subNode->keys[childIndex] = LeftNode->keys[LeftNode->n - 1];
(LeftNode->n)--;
(deleteNode->n)++;
BTree_delete_key(deleteNode,k);
}
//如果右兄弟關鍵字大於T,將父結點中的第childIndex個元素送給deleteNode,將Right中的最小元素送給父結點,
else if(childIndex<subNode->n && RightNode->n >= T)
{
deleteNode->keys[deleteNode->n] = subNode->keys[childIndex];
subNode->keys[childIndex] = RightNode->keys[0];
for(int i=0;i<RightNode->n-1;i++)
RightNode[i] = RightNode[i+1];
(RightNode->n)--;
(deleteNode->n)++;
BTree_delete_key(deleteNode,k);
}
//如果左兄弟和右兄弟的關鍵字數均不在於T,則將左兄弟或右兄弟與其合併
else
{
if(childIndex>=1)//左兄弟存在,合併
{
//將keys合併
for(int i=0;i<deleteNode->n;i++)
{
LeftNode->keys[LeftNode->n+i] = deleteNode->keys[i];
}
//如果非葉子結點,則將葉子也合併
if(!deleteNode->isLeaf)
{
for(int i=0;i<deleteNode->n+1;i++)
{
LeftNode->child[LeftNode->n+1+i] = deleteNode->child[i];
}
}
LeftNode->n = LeftNode->n + deleteNode->n;
//調整subNode的子節點
for(int i = childIndex;i<subNode->n;i++)
{
subNode->child[i] = subNode->child[i+1];
}
BTree_delete_key(LeftNode,k);
}
else //合併它和右兄弟
{
//將keys合併
for(int i=0;i<RightNode->n;i++)
{
deleteNode->keys[i+deleteNode->n] = RightNode->keys[i];
}
//如果非葉子結點,則將葉子合併
if(!deleteNode->isLeaf)
{
for(int i = 0;i<RightNode->n+1;i++)
{
deleteNode->child[deleteNode->n + 1 + i] = RightNode->child[i];
}
}
deleteNode->n = deleteNode->n + RightNode->n;
//調整subNode的子節點
for(int i = childIndex+1;i<subNode->n;i++)
{
subNode->child[i] = subNode->child[i+1];
}
BTree_delete_key(deleteNode,k);
}
}
}
BTree_delete_key(deleteNode,k);
}
}
void createBTree(p_B_Tree_Node *root)
{
int a[] = {12,1,9,2,0,11,7,19,4,15,18,5,14,13,10,16,6,3,8,17};
for(int i = 0;i<20;i++)
{
BTree_Insert_Node(root,a[i]);
printBFS(*root);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
B_Tree_Node *root = alloact_Node();
createBTree(&root);
cout<<"B_Tree after delete 12:"<<endl;
BTree_delete_key(root,12);
printBFS(root);
cout<<"B_Tree after delete 1:"<<endl;
BTree_delete_key(root,1);
printBFS(root);
cout<<"B_Tree after delete 9:"<<endl;
BTree_delete_key(root,9);
printBFS(root);
cout<<"B_Tree after delete 2:"<<endl;
BTree_delete_key(root,2);
printBFS(root);
return 0;
}