數據結構:B樹、B+樹詳解和C語言實現
B樹的定義
什麼是B樹
- B樹是專門爲磁盤或其他直接存取的輔助存儲設備設計的一類平衡查找樹,可以實現O(logN)時間複雜度的存取操作。
- 不同於紅黑樹,B樹的每個節點可以存放多個數據。根據每個節點存放數據的多少,可以把B樹分爲不同的階數。
爲什麼要用B樹
- B樹查找數據速度快,和紅黑樹等平衡二叉查找樹相當,同時由於B樹的每個節點可以包含多個數據關鍵字,相當於對數的底變大,查找的深度變小,減少了對磁盤的存取操作的次數。
B樹的性質
我們按照如下性質,來定義一棵非空的m階B樹(m>2,2階B樹相當於二叉平衡查找樹):
- 每個非葉子節點中存放若干關鍵字數據,並且有若干指向兒子節點的指針。指針數目=關鍵字數目+1
- 根節點有最少1個,最多m-1個關鍵字,最少2個,最多m個子節點。
- 非根節點最少有[m/2](向上取整),最多m-1個關鍵字
- 每個節點中的關鍵字從左到右以非降序排列
- 每個關鍵字均不小於其左子節點的關鍵字,不大於其右子節點的所有關鍵字
- 每個葉子節點都具有相同的深度
B+樹
- B+樹在B樹的基礎上進行了改進,在把所有的附加數據信息都存在葉子節點中,而非葉子節點只是存放其每顆子樹中最大的關鍵字和指針,從而大大減小了內部節點的空間佔用,使得一個磁盤塊可以容納更多的內部節點,減少了磁盤IO操作的次數,提高效率。
- 並且B+樹的葉子結點本身依關鍵字的大小自小而大順序鏈接,相當於一個順序鏈表。需要範圍查找的時候,b+樹只需遍歷葉子節點鏈表即可,b樹卻需要重複地中序遍歷。
B+樹與B樹的區別
- 在B+樹中,具有n個關鍵字的結點只含有n棵子樹,即每個關鍵字對應一棵子樹;而在B樹中,具有n個關鍵字的結點含有(n+1)棵子樹。
- 在B+樹中,每個結點(非根結點)關鍵字個數n的範圍是[m/2向上取整,m](根結點:1<=n<=m),在B樹中,每個結點(非根結點)關鍵字個數n的範圍是[m/2向上取整-1,m-1](根結點:1<=n<=m-1)。
- 在B+樹中,葉結點包含信息,所有非葉結點僅起到索引作用,非葉結點中的每個索引項只含有對應子樹的最大關鍵字和指向該子樹的指針,不含有該關鍵字對應記錄的存儲地址。
- 在B+樹中,葉結點包含了全部關鍵字,即在非葉結點中出現的關鍵字也會出現在葉結點中;而在B樹中,葉結點包含的關鍵字和其他結點包含的關鍵字是不重複的。
B+樹的特點
通常在B+樹中有兩個頭指針:一個指向根結點,另一個指向關鍵字最小的葉結點。因此,可以對B+樹進行兩種查找運算:一種是從最小關鍵字開始的順序查找,另一種是從根結點開始,進行多路查找。
C語言實現
數據結構定義
#define ORDER 3 //B樹的階數
typedef int KeyType; //關鍵字數據類型
typedef struct BTNode //B樹結點
{
int keynum; /// 結點中關鍵字的個數
KeyType key[ORDER-1]; /// 關鍵字數組,長度=階數-1
struct BTNode* child[ORDER]; /// 孩子指針數組,長度=階數
int isLeaf; /// 是否是葉子節點的標誌
}BTNode;
typedef BTNode* BTree; ///定義BTree
創建B樹
初始化一個空結點
BTree BTree_init()
{
BTree bt =(BTree)calloc(1,sizeof(BTNode));
bt->isLeaf = 1;
bt->keynum = 0;
bt->parent = NULL;
return bt;
}
根據給定的數據集合進行建樹
void BTree_create(BTree *tree,const KeyType *data,int length)
{
//初始化一個B樹節點
*tree = BTree_init();
//循環插入
for(int i = 0;i<length;i++)
{
BTree_insert(tree,data[i]);
}
}
結點插入
- 插入過程:
- 節點插從樹根開始查找,遇到葉子節點後插入。
- 如果該葉子節點是滿節點,則從中間分裂節點然後插入,相當於長高了一層。
- 爲了避免葉子節點向上分裂的時候引起其父節點的分裂,所有在從樹根向下查找的時候,就直接把遇到的所有滿節點進行分裂操作,然後對分裂後的樹執行插入操作
判斷節點是否是滿節點
int isfull(BTNode *node)
{
if(node->keynum < ORDER)
{
return 0;
}
else
{
return 1;
}
}
進行遞歸插入
void BTree_insert(BTree *tree,KeyType key)
{
BTNode *bnp = *tree;
//節點滿,直接進行分裂
if(isfull(*tree))
{
split_tree(tree);
BTree_insert(tree,key);
return;
}
//是葉子節點且不滿,直接插入
if(bnp->isLeaf && !isfull(bnp))
{
//比最大的關鍵字都大,直接插在末尾
if(key>=bnp->key[bnp->keynum -1])
{
bnp->keynum++;
bnp->key[bnp->keynum-1] = key;
}
else
{
for(int i = 0;i<ORDER-1;i++)
{
//找到一個比待插入關鍵字大的關鍵字,則直接插入
if(key< bnp->key[i] )
{
KeyType temp= bnp->key[i];
bnp->key[i] = key;
bnp->keynum++;
for(int j = i + 1;j< bnp->keynum;j++)
{
key = bnp->key[j];
bnp->key[j] = temp;
temp = key;
}
break;
}
}
}
return;
}
//不是葉子節點,查找對應的子樹,遞歸插入
if(!bnp->isLeaf)
{
for(int i = 0;i < bnp ->keynum;i++)
{
if(key<bnp->key[i])
{
BTree_insert(&((*tree)->child[i]),key);
return;
}
}
BTree_insert(&((*tree)->child[bnp->keynum]),key);
}
}
子樹的分裂
- 子樹的分裂是比較麻煩的地方,需要多申請兩個節點,進行數據的複製粘貼,注意下標的對應。
- 大致流程是如果父節點爲空,申請一個新節點,把中間的關鍵字拿出來給它,然後讓傳入的二級指針解引用後指向它。如果不空,則把中間關鍵字插入到父節點中。
- 接着再申請一個節點,把中間節點的後半部分的關鍵字和子樹指針複製過去,注意節點的性質(是否是葉子節點)也要複製。
- 然後修改原來節點的數據項,使其只保留前半部分的數據
- 最後把兩個新得到的子樹一左一右掛在新的根節點上。
void split_tree(BTree *tree)
{
BTree bnp1 = *tree;
BTree bnp2 = BTree_init();
BTree bp;
int num = bnp1->keynum;
int split = num/2;
if(bnp1->parent == NULL)
{
bp = BTree_init();
bp->parent = NULL;
bp->keynum = 1;
bp->isLeaf = 0;
bp->key[0] = bnp1->key[split];
bp->child[0] = bnp1;
bp->child[1] = bnp2;
}
else
{
bp = bnp1->parent;
bp->isLeaf = 0;
bp->keynum++;
KeyType temp1, temp2;
BTNode *tcp1, *tcp2;
for(int i = 0;i < bp->keynum;i++)
{
//新關鍵字插到末尾
if(i == bp->keynum-1)
{
bp->key[i] = bnp1->key[split];
bp->child[i] = bnp1;
bp->child[i+1] = bnp2;
break;
}
//新關鍵字插到中間
if(bp->key[i]>bnp1->key[split])
{
tcp2 = bnp2;
temp2 = bnp1->key[split];
for(int k = i;k<bp->keynum;k++)
{
//關鍵字後移
temp1 = bp->key[k];
bp->key[k] = temp2;
temp2 = temp1;
//子樹指針後移
tcp1 = bp->child[k+1];
bp->child[k+1] = tcp2;
tcp2 = tcp1;
}
}
}
}
bnp1->keynum = split;
bnp2->keynum = num - split -1;
for(int i = 0, j = split+1;j<num;j++)
{
bnp2->key[i]=bnp1->key[j];
bnp2->child[i]=bnp1->child[j];
}
bnp2->child[bnp2->keynum] = NULL;
bnp2->isLeaf = bnp1->isLeaf;
bnp2->parent = bp;
bnp1->parent = bp;
(*tree) = bp;
}