B樹
即二叉搜索樹:
1.所有非葉子結點至多擁有兩個兒子(Left和Right);
2.所有結點存儲一個關鍵字;
3.非葉子結點的左指針指向小於其關鍵字的子樹,右指針指向大於其關鍵字的子樹;
如:
B樹的搜索,從根結點開始,如果查詢的關鍵字與結點的關鍵字相等,那麼就命中;否則,如果查詢關鍵字比結點關鍵字小,就進入左兒子;如果比結點關鍵字大,就進入右兒子;如果左兒子或右兒子的指針爲空,則報告找不到相應的關鍵字;
如果B樹的所有非葉子結點的左右子樹的結點數目均保持差不多(平衡),那麼B樹的搜索性能逼近二分查找;但它比連續內存空間的二分查找的優點是,改變B樹結構(插入與刪除結點)不需要移動大段的內存數據,甚至通常是常數開銷;
如:
但B樹在經過多次插入與刪除後,有可能導致不同的結構:
右邊也是一個B樹,但它的搜索性能已經是線性的了;同樣的關鍵字集合有可能導致不同的樹結構索引;所以,使用B樹還要考慮儘可能讓B樹保持左圖的結構,和避免右圖的結構,也就是所謂的“平衡”問題;
實際使用的B樹都是在原B樹的基礎上加上平衡算法,即“平衡二叉樹”;如何保持B樹結點分佈均勻的平衡算法是平衡二叉樹的關鍵;平衡算法是一種在B樹中插入和刪除結點的策略;
B-樹
是一種多路搜索樹(並不是二叉的):
1.定義任意非葉子結點最多隻有M個兒子;且M>2;
2.根結點的兒子數爲[2, M];
3.除根結點以外的非葉子結點的兒子數爲[M/2, M];
4.每個結點存放至少M/2-1(取上整)和至多M-1個關鍵字;(至少2個關鍵字)
5.非葉子結點的關鍵字個數=指向兒子的指針個數-1;
6.非葉子結點的關鍵字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
7.非葉子結點的指針:P[1], P[2], …, P[M];其中P[1]指向關鍵字小於K[1]的子樹,P[M]指向關鍵字大於K[M-1]的子樹,其它P[i]指向關鍵字屬於(K[i-1], K[i])的子樹;
8.所有葉子結點位於同一層;
如:(M=3)
B-樹的搜索,從根結點開始,對結點內的關鍵字(有序)序列進行二分查找,如果命中則結束,否則進入查詢關鍵字所屬範圍的兒子結點;重複,直到所對應的兒子指針爲空,或已經是葉子結點;
B-樹的特性:
1.關鍵字集合分佈在整顆樹中;
2.任何一個關鍵字出現且只出現在一個結點中;
3.搜索有可能在非葉子結點結束;
4.其搜索性能等價於在關鍵字全集內做一次二分查找;
5.自動層次控制;
由於限制了除根結點以外的非葉子結點,至少含有M/2個兒子,確保了結點的至少利用率,其最底搜索性能爲:
其中,M爲設定的非葉子結點最多子樹個數,N爲關鍵字總數;
所以B-樹的性能總是等價於二分查找(與M值無關),也就沒有B樹平衡的問題;
由於M/2的限制,在插入結點時,如果結點已滿,需要將結點分裂爲兩個各佔M/2的結點;刪除結點時,需將兩個不足M/2的兄弟結點合併;
B+樹
B+樹是B-樹的變體,也是一種多路搜索樹:
1.其定義基本與B-樹同,除了:
2.非葉子結點的子樹指針與關鍵字個數相同;
3.非葉子結點的子樹指針P[i],指向關鍵字值屬於[K[i], K[i+1])的子樹(B-樹是開區間);
5.爲所有葉子結點增加一個鏈指針;
6.所有關鍵字都在葉子結點出現;
如:(M=3)
B+的搜索與B-樹也基本相同,區別是B+樹只有達到葉子結點才命中(B-樹可以在非葉子結點命中),其性能也等價於在關鍵字全集做一次二分查找;
B+的特性:
1.所有關鍵字都出現在葉子結點的鏈表中(稠密索引),且鏈表中的關鍵字恰好是有序的;
2.不可能在非葉子結點命中;
3.非葉子結點相當於是葉子結點的索引(稀疏索引),葉子結點相當於是存儲(關鍵字)數據的數據層;
4.更適合文件索引系統;
B*樹
是B+樹的變體,在B+樹的非根和非葉子結點再增加指向兄弟的指針;
B*樹定義了非葉子結點關鍵字個數至少爲(2/3)*M,即塊的最低使用率爲2/3(代替B+樹的1/2);
B+樹的分裂:當一個結點滿時,分配一個新的結點,並將原結點中1/2的數據複製到新結點,最後在父結點中增加新結點的指針;B+樹的分裂隻影響原結點和父結點,而不會影響兄弟結點,所以它不需要指向兄弟的指針;
B*樹的分裂:當一個結點滿時,如果它的下一個兄弟結點未滿,那麼將一部分數據移到兄弟結點中,再在原結點插入關鍵字,最後修改父結點中兄弟結點的關鍵字(因爲兄弟結點的關鍵字範圍改變了);如果兄弟也滿了,則在原結點與兄弟結點之間增加新結點,並各複製1/3的數據到新結點,最後在父結點增加新結點的指針;
所以,B*樹分配新結點的概率比B+樹要低,空間使用率更高;
B樹:二叉樹,每個結點只存儲一個關鍵字,等於則命中,小於走左結點,大於走右結點;
B-樹:多路搜索樹,每個結點存儲M/2到M個關鍵字,非葉子結點存儲指向關鍵字範圍的子結點;
所有關鍵字在整顆樹中出現,且只出現一次,非葉子結點可以命中;
B+樹:在B-樹基礎上,爲葉子結點增加鏈表指針,所有關鍵字都在葉子結點中出現,非葉子結點作爲葉子結點的索引;B+樹總是到葉子結點才命中;
B*樹:在B+樹基礎上,爲非葉子結點也增加鏈表指針,將結點的最低利用率從1/2提高到2/3;
實現:
1、B+樹索引的總體結構 |
圖1 B+樹的結點結構
|
2、B+樹索引的葉結點 ①指針Pi(i=1,2,…,n-1)指向具有搜索碼值Ki的一個文件記錄或一個指針(存儲)桶,桶中的每個指針指向具有搜索碼值Ki的一個文件記錄。指針桶只在文件不按搜索碼順序物理存儲時才使用。指針Pn具有特殊的作用; ②每個葉結點最多可有n-1個搜索碼值,最少也要有個搜索碼值。各個葉結點中搜索碼值的範圍互不相交。要使B+樹索引成爲稠密索引,數據文件中的各搜索碼值都必須出現在某個葉結點中且只能出現一次; ③由於各葉結點按照所含的搜索碼值有一個線性順序,所以就可以利用各個葉結點的指針Pn將葉結點按搜索碼順序鏈接在一起。這種排序能夠高效地對文件進行順序處理,而B+樹索引的其他結構能夠高效地對文件進行隨機處理,如圖2所示。 |
圖2 B+樹索引的葉結點結構示例
|
3、B+樹索引的非葉結點 ①B+樹索引的非葉結點形成葉結點上的一個多級(稀疏)索引; ②非葉結點的結構和葉結點的結構相同,即含有能夠存儲n-1個搜索碼值和n個指針的存儲單元的數據結構。只不過非葉結點中的所有指針都指向樹中的結點; ③如果一個非葉結點有m個指針,則≤m≤n。若m<n,則非葉結點中指針Pm之後的所有空閒空間作爲預留空間,與葉結點的區別在於結點的最後一個指針Pm和Pn的位置與指向不同,如圖3所示; |
圖3 B+樹索引的非葉結點結構
|
④在一個含有m個指針的非葉結點中,指針Pi(i=2,…,m-1)指向一棵子樹,該子樹的所有結點的搜索碼值大於等於Ki-1而小於Ki。指針Pm指向子樹中所含搜索碼值大於等於Km-1的那一部分,而指針P1指向子樹中所含搜索碼值小於K1的那一部分,如圖4所示。 |
圖4 B+樹索引的非葉結點中指針Pi的指向
|
4、B+樹索引的根結點 ①根結點的結構也與葉結點相同; ②根結點包含的指針數可以小於。但是,除非整棵樹只有一個結點,否則根結點必須至少包含兩個指針。圖5給出一個B+樹結構的示意圖。 |
圖5 account關係的B+樹索引結構
5、部分操作的C語言實現
/* btrees.h */
/* * 平衡多路樹的一種重要方案。 * 在 1970 年由 R. Bayer 和 E. McCreight 發明。 */ #define M 1 /* B 樹的階,即非根節點中鍵的最小數目。 * 有些人把階定義爲非根節點中子樹的最大數目。 */ typedef int typekey; typedef struct btnode { /* B-Tree 節點 */ int d; /* 節點中鍵的數目 */ typekey k[2*M]; /* 鍵 */ char *v[2*M]; /* 值 */ struct btnode *p[2*M+1]; /* 指向子樹的指針 */ } node, *btree; /* * 每個鍵的左子樹中的所有的鍵都小於這個鍵, * 每個鍵的右子樹中的所有的鍵都大於等於這個鍵。 * 葉子節點中的每個鍵都沒有子樹。 */ /* 當 M 等於 1 時也稱爲 2-3 樹 * +----+----+ * | k0 | k1 | * +-+----+----+--- * | p0 | p1 | p2 | * +----+----+----+ */ extern int btree_disp; /* 查找時找到的鍵在節點中的位置 */ extern char * InsValue; /* 與要插的鍵相對應的值 */ extern btree search(typekey, btree); extern btree insert(typekey,btree); extern btree delete(typekey,btree); extern int height(btree); extern int count(btree); extern double payload(btree); extern btree deltree(btree); /* end of btrees.h */ /*******************************************************/ /* btrees.c */ #include #include #include "btrees.h" btree search(typekey, btree); btree insert(typekey,btree); btree delete(typekey,btree); int height(btree); int count(btree); double payload(btree); btree deltree(btree); static void InternalInsert(typekey, btree); static void InsInNode(btree, int); static void SplitNode(btree, int); static btree NewRoot(btree); static void InternalDelete(typekey, btree); static void JoinNode(btree, int); static void MoveLeftNode(btree t, int); static void MoveRightNode(btree t, int); static void DelFromNode(btree t, int); static btree FreeRoot(btree); static btree delall(btree); static void Error(int,typekey); int btree_disp; /* 查找時找到的鍵在節點中的位置 */ char * InsValue = NULL; /* 與要插的鍵相對應的值 */ static int flag; /* 節點增減標誌 */ static int btree_level = 0; /* 多路樹的高度 */ static int btree_count = 0; /* 多路樹的鍵總數 */ static int node_sum = 0; /* 多路樹的節點總數 */ static int level; /* 當前訪問的節點所處的高度 */ static btree NewTree; /* 在節點分割的時候指向新建的節點 */ static typekey InsKey; /* 要插入的鍵 */ btree search(typekey key, btree t) { int i,j,m; level=btree_level-1; while (level >= 0){ for(i=0, j=t->d-1; i t->k[m])?(i=m+1):(j=m)); if (key == t->k){ btree_disp = i; return t; } if (key > t->k) /* i == t->d-1 時有可能出現 */ i++; t = t->p; level--; } return NULL; } btree insert(typekey key, btree t) { level=btree_level; InternalInsert(key, t); if (flag == 1) /* 根節點滿之後,它被分割成兩個半滿節點 */ t=NewRoot(t); /* 樹的高度增加 */ return t; } void InternalInsert(typekey key, btree t) { int i,j,m; level--; if (level < 0){ /* 到達了樹的底部: 指出要做的插入 */ NewTree = NULL; /* 這個鍵沒有對應的子樹 */ InsKey = key; /* 導致底層的葉子節點增加鍵值+空子樹對 */ btree_count++; flag = 1; /* 指示上層節點把返回的鍵插入其中 */ return; } for(i=0, j=t->d-1; i t->k[m])?(i=m+1):(j=m)); if (key == t->k) { Error(1,key); /* 鍵已經在樹中 */ flag = 0; return; } if (key > t->k) /* i == t->d-1 時有可能出現 */ i++; InternalInsert(key, t->p); if (flag == 0) return; /* 有新鍵要插入到當前節點中 */ if (t->d < 2*M) {/* 當前節點未滿 */ InsInNode(t, i); /* 把鍵值+子樹對插入當前節點中 */ flag = 0; /* 指示上層節點沒有需要插入的鍵值+子樹,插入過程結束 */ } else /* 當前節點已滿,則分割這個頁面並把鍵值+子樹對插入當前節點中 */ SplitNode(t, i); /* 繼續指示上層節點把返回的鍵值+子樹插入其中 */ } /* * 把一個鍵和對應的右子樹插入一個節點中 */ void InsInNode(btree t, int d) { int i; /* 把所有大於要插入的鍵值的鍵和對應的右子樹右移 */ for(i = t->d; i > d; i--){ t->k = t->k[i-1]; t->v = t->v[i-1]; t->p[i+1] = t->p; } /* 插入鍵和右子樹 */ t->k = InsKey; t->p[i+1] = NewTree; t->v = InsValue; t->d++; } /* * 前件是要插入一個鍵和對應的右子樹,並且本節點已經滿 * 導致分割這個節點,插入鍵和對應的右子樹, * 並向上層返回一個要插入鍵和對應的右子樹 */ void SplitNode(btree t, int d) { int i,j; btree temp; typekey temp_k; char *temp_v; /* 建立新節點 */ temp = (btree)malloc(sizeof(node)); /* * +---+--------+-----+-----+--------+-----+ * | 0 | ...... | M | M+1 | ...... |2*M-1| * +---+--------+-----+-----+--------+-----+ * |<- M+1 ->|<- M-1 ->| */ if (d > M) { /* 要插入當前節點的右半部分 */ /* 把從 2*M-1 到 M+1 的 M-1 個鍵值+子樹對轉移到新節點中, * 並且爲要插入的鍵值+子樹空出位置 */ for(i=2*M-1,j=M-1; i>=d; i--,j--) { temp->k[j] = t->k; temp->v[j] = t->v; temp->p[j+1] = t->p[i+1]; } for(i=d-1,j=d-M-2; j>=0; i--,j--) { temp->k[j] = t->k; temp->v[j] = t->v; temp->p[j+1] = t->p[i+1]; } /* 把節點的最右子樹轉移成新節點的最左子樹 */ temp->p[0] = t->p[M+1]; /* 在新節點中插入鍵和右子樹 */ temp->k[d-M-1] = InsKey; temp->p[d-M] = NewTree; temp->v[d-M-1] = InsValue; /* 設置要插入上層節點的鍵和值 */ InsKey = t->k[M]; InsValue = t->v[M]; } else { /* d <= M */ /* 把從 2*M-1 到 M 的 M 個鍵值+子樹對轉移到新節點中 */ for(i=2*M-1,j=M-1; j>=0; i--,j--) { temp->k[j] = t->k; temp->v[j] = t->v; temp->p[j+1] = t->p[i+1]; } if (d == M) /* 要插入當前節點的正中間 */ /* 把要插入的子樹作爲新節點的最左子樹 */ temp->p[0] = NewTree; /* 直接把要插入的鍵和值返回給上層節點 */ else { /* (d /* 把節點當前的最右子樹轉移成新節點的最左子樹 */ temp->p[0] = t->p[M]; /* 保存要插入上層節點的鍵和值 */ temp_k = t->k[M-1]; temp_v = t->v[M-1]; /* 把所有大於要插入的鍵值的鍵和對應的右子樹右移 */ for(i=M-1; i>d; i--) { t->k = t->k[i-1]; t->v = t->v[i-1]; t->p[i+1] = t->p; } /* 在節點中插入鍵和右子樹 */ t->k[d] = InsKey; t->p[d+1] = NewTree; t->v[d] = InsValue; /* 設置要插入上層節點的鍵和值 */ InsKey = temp_k; InsValue = temp_v; } } t->d =M; temp->d = M; NewTree = temp; node_sum++; } btree delete(typekey key, btree t) { level=btree_level; InternalDelete(key, t); if (t->d == 0) /* 根節點的子節點合併導致根節點鍵的數目隨之減少, * 當根節點中沒有鍵的時候,只有它的最左子樹可能非空 */ t=FreeRoot(t); return t; } void InternalDelete(typekey key, btree t) { int i,j,m; btree l,r; int lvl; level--; if (level < 0) { Error(0,key); /* 在整個樹中未找到要刪除的鍵 */ flag = 0; return; } for(i=0, j=t->d-1; i t->k[m])?(i=m+1):(j=m)); if (key == t->k) { /* 找到要刪除的鍵 */ if (t->v != NULL) free(t->v); /* 釋放這個節點包含的值 */ if (level == 0) { /* 有子樹爲空則這個鍵位於葉子節點 */ DelFromNode(t,i); btree_count--; flag = 1; /* 指示上層節點本子樹的鍵數量減少 */ return; } else { /* 這個鍵位於非葉節點 */ lvl = level-1; /* 找到前驅節點 */ r = t->p; while (lvl > 0) { r = r->p[r->d]; lvl--; } t->k=r->k[r->d-1]; t->v=r->v[r->d-1]; r->v[r->d-1]=NULL; key = r->k[r->d-1]; } } else if (key > t->k) /* i == t->d-1 時有可能出現 */ i++; InternalDelete(key,t->p); /* 調整平衡 */ if (flag == 0) return; if (t->p->d < M) { if (i == t->d) /* 在最右子樹中發生了刪除 */ i--; /* 調整最右鍵的左右子樹平衡 */ l = t->p; r = t->p[i+1]; if (r->d > M) MoveLeftNode(t,i); else if(l->d > M) MoveRightNode(t,i); else { JoinNode(t,i); /* 繼續指示上層節點本子樹的鍵數量減少 */ return; } flag = 0; /* 指示上層節點本子樹的鍵數量沒有減少,刪除過程結束 */ } } /* * 合併一個節點的某個鍵對應的兩個子樹 */ void JoinNode(btree t, int d) { btree l,r; int i,j; l = t->p[d]; r = t->p[d+1]; /* 把這個鍵下移到它的左子樹 */ l->k[l->d] = t->k[d]; l->v[l->d] = t->v[d]; /* 把右子樹中的所有鍵值和子樹轉移到左子樹 */ for (j=r->d-1,i=l->d+r->d; j >= 0 ; j--,i--) { l->k = r->k[j]; l->v = r->v[j]; l->p = r->p[j]; } l->p[l->d+r->d+1] = r->p[r->d]; l->d += r->d+1; /* 釋放右子樹的節點 */ free(r); /* 把這個鍵右邊的鍵和對應的右子樹左移 */ for (i=d; i < t->d-1; i++) { t->k = t->k[i+1]; t->v = t->v[i+1]; t->p[i+1] = t->p[i+2]; } t->d--; node_sum--; } /* * 從一個鍵的右子樹向左子樹轉移一些鍵,使兩個子樹平衡 */ void MoveLeftNode(btree t, int d) { btree l,r; int m; /* 應轉移的鍵的數目 */ int i,j; l = t->p[d]; r = t->p[d+1]; m = (r->d - l->d)/2; /* 把這個鍵下移到它的左子樹 */ l->k[l->d] = t->k[d]; l->v[l->d] = t->v[d]; /* 把右子樹的最左子樹轉移成左子樹的最右子樹 * 從右子樹向左子樹移動 m-1 個鍵+子樹對 */ for (j=m-2,i=l->d+m-1; j >= 0; j--,i--) { l->k = r->k[j]; l->v = r->v[j]; l->p = r->p[j]; } l->p[l->d+m] = r->p[m-1]; /* 把右子樹的最左鍵提升到這個鍵的位置上 */ t->k[d] = r->k[m-1]; t->v[d] = r->v[m-1]; /* 把右子樹中的所有鍵值和子樹左移 m 個位置 */ r->p[0] = r->p[m]; for (i=0; id-m; i++) { r->k = r->k[i+m]; r->v = r->v[i+m]; r->p = r->p[i+m]; } r->p[r->d-m] = r->p[r->d]; l->d+=m; r->d-=m; } /* * 從一個鍵的左子樹向右子樹轉移一些鍵,使兩個子樹平衡 */ void MoveRightNode(btree t, int d) { btree l,r; int m; /* 應轉移的鍵的數目 */ int i,j; l = t->p[d]; r = t->p[d+1]; m = (l->d - r->d)/2; /* 把右子樹中的所有鍵值和子樹右移 m 個位置 */ r->p[r->d+m]=r->p[r->d]; for (i=r->d-1; i>=0; i--) { r->k[i+m] = r->k; r->v[i+m] = r->v; r->p[i+m] = r->p; } /* 把這個鍵下移到它的右子樹 */ r->k[m-1] = t->k[d]; r->v[m-1] = t->v[d]; /* 把左子樹的最右子樹轉移成右子樹的最左子樹 */ r->p[m-1] = l->p[l->d]; /* 從左子樹向右子樹移動 m-1 個鍵+子樹對 */ for (i=l->d-1,j=m-2; j>=0; j--,i--) { r->k[j] = l->k; r->v[j] = l->v; r->p[j] = l->p; } /* 把左子樹的最右鍵提升到這個鍵的位置上 */ t->k[d] = l->k; t->v[d] = l->v; l->d-=m; r->d+=m; } /* * 把一個鍵和對應的右子樹從一個節點中刪除 */ void DelFromNode(btree t, int d) { int i; /* 把所有大於要刪除的鍵值的鍵左移 */ for(i=d; i < t->d-1; i++) { t->k = t->k[i+1]; t->v = t->v[i+1]; } t->d--; } /* * 建立有兩個子樹和一個鍵的根節點 */ btree NewRoot(btree t) { btree temp; temp = (btree)malloc(sizeof(node)); temp->d = 1; temp->p[0] = t; temp->p[1] = NewTree; temp->k[0] = InsKey; temp->v[0] = InsValue; btree_level++; node_sum++; return(temp); } /* * 釋放根節點,並返回它的最左子樹 */ btree FreeRoot(btree t) { btree temp; temp = t->p[0]; free(t); btree_level--; node_sum--; return temp; } void Error(int f,typekey key) { if (f) printf("Btrees error: Insert %d!/n",key); else printf("Btrees error: delete %d!/n",key); } int height(btree t) { return btree_level; } int count(btree t) { return btree_count; } double payload(btree t) { if (node_sum==0) return 1; return (double)btree_count/(node_sum*(2*M)); } btree deltree (btree t) { level=btree_level; btree_level = 0; return delall(t); } btree delall(btree t) { int i; level--; if (level >= 0) { for (i=0; i < t->d; i++) if (t->v != NULL) free(t->v); if (level > 0) for (i=0; i<= t->d ; i++) t->p=delall(t->p); free(t); } return NULL; } /* end of btrees.c */ |