本代碼爲參考算法導論所寫,主要記錄B樹數據結構的實現原理及方式。
本代碼主要實現了B樹的插入和刪除的操作過程。詳細註釋了插入分裂,刪除合併的邏輯規則。
本代碼未講過仔細的調試和詳盡的測試,但可以作爲學習和研究B樹結構原理,實現方式的參考。
package tree;
/**
* B樹特徵
* 每個節點必須包含 t-1 -> 2t-1個key,t>=2。根節點可以只有1個key. t被稱爲最小度數
* 每個節點需包含 t -> 2t個子節點指針。 這樣保證了樹至少爲2叉的。
* 每個節點的關鍵字以非降序排序
* 每個葉節點具有相同的深度,所以是平衡的。樹高爲 h <= logt((n+1)/2), n爲樹的總關鍵字數,
*
* B樹操作
* B樹通過插入的分裂,刪除的合併來保證樹一直滿足B樹的特徵。
* 當插入時,由於子節已滿,需要分裂成兩個子節點,每個子節點含有t-1個關鍵字,並在父節點中插入原節點中間值
* 在插入過程中,從根節點向下遞歸查找時就要確保根節點,及遞歸時的子節點爲非滿的,否則進行跟/子節點分裂。
* 由於分裂操作,並不會影響每個節點的樹高,只有在跟節點進行分裂的時候樹高才加1,子節點高度依次增加,所以B樹總是平衡的。
*
* 在刪除過程中,可以分爲兩種情況
* 1. 刪除的爲葉子節點關鍵字。
* 如果葉子節點關鍵字數量大於t-1,則直接刪除即可。
* 如果不足t-1則需要從父節點移動關鍵字下來,然後遞歸刪除父節點關鍵字。
*
* 2. 刪除的爲內部節點關鍵字。
* 如果相鄰兩個子節點有一個具有大於t-1的關鍵字,則直接從該節點移動關鍵字上來填充即可。然後向下遞歸刪除子節點關鍵字
* 如果相鄰兩個子節點都只有t-1個關鍵字,則操作子節點進行合併。然後刪除父節點關鍵字。如果由於刪除父節點關鍵字導致關鍵字數量不足t-1,
* 則需要繼續從祖節點移動關鍵字下來,然後遞歸刪除祖節點相應關鍵字。
*
* 從刪除操作看,刪除操作可能在樹中不斷向下遞歸移動關鍵字到父節點,或者向上回溯移動關鍵字到子節點。但每步操作後都符合B樹的特徵,
* 並在這個過程中使得相關子節點合併爲2t-2個關鍵字的新節點。
*
*
* B+樹特徵
* B樹的變種,在非葉子節點不能存儲關鍵字的值,所有數據存儲在葉子節點,因此最大化了內部節點的分支因子
* 由於B樹刪除內部節點的複雜性,需要向下遞歸移動關鍵字,同時可能向上回溯進行合併
* 所以B+樹將所有關鍵字數據放在葉子節點能最大化刪除操作的性能,直接找到某個葉子節點進行刪除,然後回溯合併即可。
*
* B樹的用處
* B樹也是一個排序樹,其較平衡二叉樹有更多的分支,所以B樹有更小的樹高,看起來更扁平,從而減少了從根節點開始的查找次數
* B樹常用於對磁盤文件的索引。由於訪問一次節點就需要進行一次磁盤I/O,所以通過減小樹高來減少磁盤I/O次數。
* @author 7527933520
*
*/
public class BTree {
private static Node root = null;
// 指明關鍵字的最小度數
private static final int t = 4;
/**
* 假設當前節點的 t = 4
* 即包含 3 - 7 個關鍵字
* 即包含 4 - 8 個子節點指針
*
* 爲了方便比較和查找,此處key使用int數組類型。
* 由於需要對key進行迭代和插入刪除操作,建議使用鏈表類型
*
* @author 75279
*
*/
class Node{
Object[] vals;
int[] keys;
Node[] child;
boolean leaf = true; // 標識該節點是否葉節點
int n = 0; // 用於記錄節點數量
Node(){
keys = new int[ 2 * t -1];
vals = new Object[ 2 * t -1];
child = new Node[ 2 * t];
}
// 判斷該節點是否已滿, 如果已經滿了,在插入的過程中則需要分裂
boolean isFull() {
return n >= (2 * t -1);
}
// 按照節點的順序插入一個節點,數組插入排序
void insert(int key, Object value, Node newChild) {
for(int i = 0; i < keys.length; i ++) {
if( key > keys[i]) {
i++;
} else { // 移動keys然後填入
transplantKey(i);
keys[i] = key;
vals[i] = value;
child[i] = newChild;
break;
}
}
n++;
}
void delete(int i) {
for(; i< n; i++) {
keys[i] = keys[i+1];
vals[i] = vals[i+1];
child[i] = child[i+1];
}
n--;
}
void transplantKey(int pivot) {
for(int i = n; i > pivot; i-- ) {
keys[i] = keys[i-1];
vals[i] = vals[i-1];
child[i+1] = child[i];
}
}
}
/**
* 開放調用
* @param val
* @return
* @throws Exception
*/
// 查找某個關鍵字
public Object search(int val) throws Exception{
if(root == null) {
return root;
}
return searchBTree(val, root); // 建議做保護性拷貝
}
// 添加一個關鍵字
public void addKey(int key, Object value) {
if(root == null) {
root = new Node();
root.keys[0] = key;
root.vals[0] = value;
root.n = 1;
return ;
} else if(root.isFull()) { // 根節點分裂
Node newRoot = new Node();
newRoot.leaf = false;
newRoot.n = 0;
newRoot.child[0]= root;
splitChild(newRoot, root);
root = newRoot;
}
// 如果跟節點非空非滿,則調用封閉方法進行插入
addKey(key, value, root);
}
/**
* 刪除某個關鍵字
* @param key
*/
public void deleteKey(int key) {
if(root == null) {
throw new RuntimeException("B樹不存在");
}
deleteKey(key, root, null, -1);
}
private void deleteKey(int key, Node node, Node parent, int pos) {
int i = 0;
while(i< node.n && i> node.keys[i]) {
i++;
}
if(i < node.n && key == node.keys[i]) {
if(node.leaf) {
if(node.n > t-1) {
// 該葉子節點刪除後能維持B樹特徵,直接刪除
node.delete(i);
return;
} else { // 從父節點移動關鍵字到子節點,然後遞歸刪除父節點的關鍵字
deleteAndFix(node, parent, i, pos);
return;
}
} else { // 刪除一個內部節點關鍵字
// 看是否能直接從子節點移動一個關鍵字上來
Node lch = node.child[i];
Node rch = node.child[i+1];
// 左邊子節點數量足夠 從左子節點移動最大的關鍵字上來
if(lch.n > t-1) {
node.keys[i] = lch.keys[lch.n -1];
node.vals[i] = lch.vals[lch.n -1];
// 遞歸的刪除子節點關鍵字
deleteKey(lch.keys[lch.n-1]);
} else if(rch.n > t-1) {
// 從右子節點移動一個關鍵字上來
node.keys[i] = rch.keys[lch.n -1];
node.vals[i] = rch.vals[lch.n -1];
// 遞歸的刪除子節點關鍵字
deleteKey(rch.keys[rch.n-1]);
} else {
// 兩邊子節點關鍵字數量都爲 t-1 需要操作合併
int j = 0;
for(; j < rch.n; j++) {
lch.vals[j + lch.n] = rch.vals[j];
lch.keys[j + lch.n] = rch.keys[j];
lch.child[j + lch.n] = rch.child[j];
}
lch.child[j + lch.n + 1] = rch.child[j + 1];
rch = null; // 刪除右結點 help gc
node.child[i+1] = lch; // 指向新子節點位置
// 如果由於本節點刪除關鍵字後數量小於t-1,則需要從父節點移動關鍵字下來
if(node.n < t-1) {
deleteAndFix(node, parent, i, pos);
}
}
}
}else { // 繼續尋找刪除關鍵字的位置
deleteKey(key, node.child[i], node, i);
}
}
private void deleteAndFix(Node node, Node parent, int delPos, int pPos) {
// 刪除指定位置的節點
node.delete(delPos);
// 如果當前爲根節點,則無需移動。因爲跟節點允許小於t-1個關鍵字
if(parent == null) {
return;
}
// 將父節點相關位置的關鍵字移動下來
if(node == parent.child[pPos]) {
// 如果是父節點關鍵字的左邊子節點, 則將關鍵字移動到子節點最大關鍵字位置
node.keys[node.n-1] = parent.keys[pPos];
node.vals[node.n-1] = parent.vals[pPos];
} else {
// 將父節點關鍵字移動到子節點最小關鍵字位置
node.insert(parent.keys[pPos], parent.vals[pPos], null);
}
// 遞歸刪除父節點關鍵字
deleteKey(parent.keys[pPos]);
return;
}
/**
* 插入節點,並在尋找插入位置的過程中,進行分裂操作
* 新關鍵字的插入,總髮生在葉子節點,如果葉子節點滿了,需要提前進行分裂然後插入。
* 此方法需要確保根節點是一個非滿的節點。
* @param key
* @param val
* @param node
*/
private void addKey(int key, Object val, Node node) {
if(node.leaf) { // 葉節點 可以放心的插入
node.insert(key, val, null);
} else { // 內部節點可能需要進行子節點分裂, 如果子節點非滿,則插入到子節點中
int i = 0;
while(i < node.n && key > node.keys[i]) {
i++;
}
Node ch = node.child[i];
if(ch.isFull()) {
splitChild(node, ch);
addKey(key, val, node);
} else {
addKey(key,val,ch);
}
}
}
/**
* 分裂一個爲滿節點的子結點
* @param x 父節點
* @param i 父節點指針位置
*/
private void splitChild(Node x, Node child) {
Node newChild = new Node();
newChild.leaf = child.leaf;
newChild.n = t - 1;
transplantToNewNode(child, newChild);
// 惰性刪除子結點多於的關鍵字,等待被覆蓋
child.n = t-1;
// 父節點插入一個新的關鍵字,該關鍵字爲字節點的第t個關鍵字
x.insert(child.keys[t], child.vals[t], newChild);
}
private void transplantToNewNode(Node old, Node fresh) {
// 將後面t-1個key 移動到新的節點上去
for(int j = 0; j < t-1; j++) {
fresh.keys[j] = old.keys[ j + t];
fresh.vals[j] = old.vals[ j + t];
// 如果不是葉節點,需要移動指針
if(!old.leaf) {
fresh.child[j] = old.child[j + t];
}
}
old.n = t-1;
fresh.n = t-1;
}
private Node searchBTree(int val, Node node) {
Node result = null;
// 遍歷節點所有的key找到相等的
int i =0;
while(i < node.n && val > node.keys[i]) { // 由於Key是非降序排序的
i++;
}
if( i < node.keys.length && val == node.keys[i]) {
result = node;
} else if(node.leaf){
result = null;
} else {
result = searchBTree(val, node.child[i]);
}
return result;
}
}