B樹原理及java實現

本代碼爲參考算法導論所寫,主要記錄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;
	}

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章