B樹的研究(查詢、插入、刪除)
講到B樹那就不得不談一談查詢,查詢不是一種資料結構而是一種基於資料結構的對資料進行處理時經常使用的一種操作。平均查詢長度ASL=∑Pi×Ci (其中Pi為查詢第i個記錄的概率,Ci為查詢第i個記錄所用的比較次數)。 查詢方法有很多,分為兩大類:靜態查詢和動態查詢。
1、 靜態查詢:只能進行查詢操作,無法進行插入和刪除操作,表現為線性結構,有:順序儲存結構和鏈式儲存結構。主要有:順序查詢、二分查詢和索引查詢。
2、 動態查詢:既能插入和刪除,又能比較高效率的實現查詢操作,一般採用樹形連結串列等非線性結構,主要有:二叉排序樹、平衡二叉樹、紅黑樹、B樹、B+樹、tire樹。
(以下內容引用網路)
3、
紅黑樹:平衡二叉樹,通過對任何一條從根到葉子的簡單路徑上各個節點的顏色進行約束,確保沒有一條路徑會比其他路徑長2倍,因而是近似平衡的。所以相對於嚴格要求平衡的AVL樹來說,它的旋轉保持平衡次數較少。用於搜尋時,插入刪除次數多的情況下我們就用紅黑樹來取代AVL。
(現在部分場景使用跳錶來替換紅黑樹,可搜尋“為啥 redis 使用跳錶(skiplist)而不是使用 red-black?”)
4、 B樹,B+樹:它們特點是一樣的,是多路查詢樹,一般用於資料庫系統中,為什麼,因為它們分支多層數少,都知道磁碟IO是非常耗時的,而像大量資料儲存在磁碟中所以我們要有效的減少磁碟IO次數避免磁碟頻繁的查詢。B+樹是B樹的變種樹,有n棵子樹的節點中含有n個關鍵字,每個關鍵字不儲存資料,只用來索引,資料都儲存在葉子節點。是為檔案系統而生。
5、 Trie樹:又名單詞查詢樹,一種樹形結構,常用來操作字串。它是不同字串的相同字首只儲存一份。相對直接儲存字串肯定是節省空間的,但是它儲存大量字串時會很耗費記憶體(是記憶體)。類似的有字首樹(prefix
tree),字尾樹(suffix tree),radix tree(patricia tree, compact prefix
tree),crit-bit tree(解決耗費記憶體問題),以及前面說的double array trie。
用階定義的B樹(國內代表:嚴蔚敏版)(m階B樹):
1.樹中每個節點最多m個孩子,即m滿足[m/2]<=M<=m;
2.除根節點和葉子節點外,其他每個節點至少[m/2]個孩子;
3.除根節點外,(包括葉子節點)節點關鍵字數n需要滿足:[m/2]-1<=n<=m-1;
用度數定義的B樹(演算法導論版)(2T階B樹):
1. B樹中每一節點包含關鍵字數有一上界和下界,此下界可用一稱作B樹的最小度數T表示;
2. 非根節點孩子數滿足:T到2T;
3. 非根節點關鍵字數滿足:T-1到2T-1;
樹高度:h<=logT*(n+1)/2, 時間複雜度為:O(logn).
B樹節點定義如下:
Typedef struct BTNode{
Int n; //關鍵字個數
Bool isleaf; //是否為葉子節點
Int *keys; //關鍵字陣列
Struct BTNode **child; //節點的孩子們
Struct BTNode *p;}BTNode,*p_BTNode; //節點的父節點
B樹查詢:
B樹查詢類似於二叉樹查詢但是B樹是進行N+1路分支的判斷,所以有所區別,具體程式碼如下:
BTNode*search(BTNode*curnode,int k,int &index)
{
int i=0;
While(i<=curnod->n&&k>curnode->keys[i])
I++;
If(i<curnode->n&&k==curnod->keys[i])
{
Index=i;
Return curnod;}
If (curnode->isleaf)
Return NULL;
Search(curnode->child[i],k,index)}
B樹插入(涉及分裂)
B樹插入操作是將關鍵字插入已存在的葉節點上,沿著根節點一直向下查詢,找到合適節點插入,若插入後節點已經滿了,即(2T階樹節點最多2T-1個關鍵字),故將n=2T-1分裂為兩個T-1個關鍵子的子節點,同時中間元素被提升至父節點,若父節點滿了,則進行迭代。
具體步驟:
1、 插入若樹空,則在葉子節點直接插入,否則非葉子節點,若該節點關鍵字數小於2T-1則插在該節點左邊或者右邊;
2、 若該節點關鍵字數為2T-1,則插入後分裂為2個T-1個關鍵字的子節點,且中間關鍵字上移至父節點(當然相應的指標也要發生改變)
3、 若2中使得父節點或根節點滿了,則根節點(父節點)也要分裂重複2使得各節點關鍵字滿足(T到2T-1)
(為了簡化演算法,採用從根節點往下查詢時,遇見關鍵字滿的節點,就進行分裂,這樣就保證了再次插入後,若節點分裂則父節點一定不為滿)
插入示例:
B樹的刪除(涉及合併)(引用)
B樹的刪除操作比插入稍微複雜一些,因為關鍵詞 可以從任意節點中刪除
(包括內節點),刪除會導致節點含關鍵詞數目過少(小於t-1),所以可能需要進行合併。 B樹中從根節點至下刪除關鍵詞主要包括以下幾種情況(
最小度數為t )。 1) 若關鍵詞k在節點x中且x為葉節點,則從x中刪除k; 2) 若關鍵詞k在節點x中且x為內節點,做以下操作:
a) 節點x中位於k之前或之後的子節點y至少有t個關鍵詞,則找出k在以y為根的子樹中的前驅或後繼k,遞迴刪除k
並在x中用k`代替k;
b)
若節點x中位於k之前或之後的子節點y和z都只有t-1個關鍵詞,則將k和z中所有關鍵詞合併進y(使y有2t-1個關鍵詞),再刪除z節點,刪除k關鍵詞。
3) 若關鍵詞k不在內節點x中,並確定必包含k的子樹的根cx[i],若cx[i]只有t-1個關鍵詞,則做以下操作: a)
若cx[i]的一個相鄰兄弟至少有t個關鍵詞,則將x中某關鍵詞降至cx[i]中,將cx[i]的相鄰兄弟節點的某關鍵詞提升至x,並修改指標;
b) 若cx[i]及其所有相鄰兄弟都只有t-1個關鍵詞,則將ci[x]與一個兄弟合併,即將x的一個關鍵詞移至新合併節點成為其中間關鍵詞。
一個包含以上情況的B樹刪除關鍵詞過程如下所示(最小度數t=2):
刪除示例:
使用說明
在本次實驗中,為了節省操作時間,偷了個懶,採用的是生成時間種子的方式,通過不斷的生成偽隨機數去構造一顆B樹,所以在實驗中,理論上不同的時間我們可以得到不同的B樹,同時,在刪除的過程中,只要相應的輸入需要刪除的關鍵字,就可以得到刪除後的新的B樹,有利於實驗過程的完整性。
執行示例:
創新點:
為了能夠實現每次執行都能夠得到不同B樹,採用生成時間種子的辦法去生成偽隨機數來作為B樹的關鍵字來進行插入,同時,為了簡化在插入後遇到分裂操作需要反覆的調整父節點的關鍵字的個數,採用了一種簡化演算法,即從根節點往下查詢時,遇見關鍵字滿的節點,就先進行分裂,這樣就能夠保證再次插入關鍵字後,若遇見葉節點關鍵字滿而分裂時,父節點一定不為滿。
有待改進:
本次實驗的B樹查詢相對簡單,插入涉及到的分裂也還好,但是在刪除的過程中,因為涉及到的合併操作並不能夠完全符合要求,所以結論就是查詢和插入沒問題,刪除只適合低階B樹,鑑於精力有限,只能夠日後有時間再仔細的研究了。
(引用)實驗程式碼:(部分參考)
#include <stdio.h>
#include<time.h>
#include<stdlib.h>
#include <iostream>
#define T 3
using namespace std;
typedef struct B_Tree_Node //b樹節點定義
{
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樹的初始化
{
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;
}
B_Tree_Node * searchNode(B_Tree_Node *curNode, int k, int &index) //關鍵字k的查詢,返回查詢資訊
{
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);
}
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)
{
newChild->isLeaf = false;
for(int i=0;i<T-1;i++)
newChild->child[i] = splitNode_p->child[i+T];
}
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) //插入節點關鍵字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);
}
}
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 << "\n【";
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]);
}
void BTree_delete_key(B_Tree_Node *subNode, int k) //刪除關鍵字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;
}
}
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];
if(processorNode->n >= T) //如果小於k的孩子結點關鍵字數大於T
{
int k1 = processorNode->keys[processorNode->n-1];
subNode->keys[keyIndex] = k1;
BTree_delete_key(processorNode,k1);
}
else if(succssorNode->n >=T) //如果大於k的孩子結點關鍵字數大於T
{
int k1 = succssorNode->keys[0];
subNode->keys[keyIndex] = k1;
BTree_delete_key(succssorNode,k1);
}
else //如果兩個孩子結點關鍵字數均不大於T,則將k與右孩子結點的關鍵字歸併到左孩子中
{
for(int j=0;j<T-1;j++)
{
processorNode->keys[processorNode->n] = k;
processorNode->keys[processorNode->n+1+j] = succssorNode->keys[j];
}
processorNode->n = 2*T -1 ;
if(!processorNode->isLeaf)
{
for(int j=0;j<T;j++)
{
processorNode->child[T+j] = succssorNode->child[j];
}
}
for(int j = keyIndex;j<subNode->n-1;j++) //修改subNode中的key值
{
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;
for(int j = 0;j<subNode->n;j++) //尋找合適的子孩子,以該子孩子為根的樹包含k
{
if(k<subNode->keys[j])
{
childIndex = j;
deleteNode = subNode->child[j];
break;
}
}
if(deleteNode->n <= T-1) //如果該子孩子的關鍵字數小於T,考慮那兩種情況
{
B_Tree_Node *LeftNode = subNode->child[childIndex-1]; //deleteNode的左兄弟結點
B_Tree_Node *RightNode = subNode->child[childIndex+1]; //deleteNode的右兄弟結點
if(childIndex>=1 && LeftNode->n >= T) //如果左兄弟結點關鍵字數大於T,將父結點中的第childIndex-1個元素送給deleteNode,將Left中的最大元素送給父結點,
{
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);
}
else if(childIndex<subNode->n && RightNode->n >= T) //如果右兄弟關鍵字大於T,將父結點中的第childIndex個元素送給deleteNode,將Right中的最小元素送給父結點,
{
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 number;
srand((unsigned)(time(NULL)));
number=rand()%11;
int a[number];
for(int i = 0;i<number;i++)
{ a[i]=rand()%200;
BTree_Insert_Node(root,a[i]);
printBFS(*root);
}
}
int main()
{
int a;
B_Tree_Node *root = alloact_Node();
createBTree(&root);
printf("你想刪除:");scanf("%d",&a);
BTree_delete_key(root,a);
printBFS(root);
printf("你還想刪除:");scanf("%d",&a);
BTree_delete_key(root,a);
printBFS(root);
printf("還想刪除:");scanf("%d",&a);
BTree_delete_key(root,a);
printBFS(root);
printf("最後一個刪除:");scanf("%d",&a);
BTree_delete_key(root,a);
printBFS(root);
return 0;
}