數據結構(三)--- B樹(B-Tree)
文章圖片代碼來自鄧俊輝老師的課件
概述
上圖就是 B-Tree 的結構,可以看到這棵樹和二叉樹有點不同---“又矮又肥”。同時子節點可以有若幹個小的子節點構成。那麽這樣一棵樹又有什麽作用呢?
動機
我們知道電腦的訪問內存比訪問外的存I/O操作快了,但是內存的容量大小又只有那麽一點點(相對於外存),所以計算機訪問的過程常常使用高速緩存。使用高速緩存也是在以下兩個事實想出的策略。
而B-Tree這種結構就是根據這種情況被發掘出來的。下圖 m 指的是每次的數據塊數量
B-Tree 介紹
多路平衡
關鍵碼指的是一個超級節點包含的子節點。
B-Tree定義
m階指的是m路,一個超級節點最大可以分出多少路。二叉樹分出兩邊,左邊和右邊,就是兩路,二階。
下面是幾個定義為不同階的B-樹。
分支數
B-Tree的分支數有個上下限,例如6階的B-Tree(m=6),又被稱為 “(3,6)-樹”,類似的還有 “(3,5)-樹”,“(2,4)-樹”,而(2,4)樹就是我們後面要學的紅黑樹。
最大樹高和最小數高
可以看到對於“含N個關鍵碼的m階B-樹”的最大樹高和最小樹高之間的波動並不大。
代碼實現
代碼實現主要的兩個方法為插入和刪除。其中插入的時候需要註意查看某個節點是否超出了階數,若超出了,需要分裂,最壞的情況就是分裂到根部,而刪除操作需要註意查看是否會產生下溢,處理下溢,我們常用的方法就是旋轉和合並。
插入
下圖分別是分裂和再分裂的圖示。
刪除
刪除操作的旋轉和合並。
旋轉可以理解為左右兄弟有足夠的節點,向左右兄弟節點借來補充的操作。
假如向兄弟們借都不成功,那就拿父節點的一個元素一起合並,代碼實現中有分左合並和右合並。
java版本代碼
B-樹節點類
package BTree; import java.util.Vector; /** * B-樹的節點Bean * 包含一個有序向量 value 和 指向子節點的 child 向量 * */ public class BTreeNode { BTreeNode parent; Vector<BTreeNode> child; Vector<Integer> value; public BTreeNode(int value, BTreeNode left, BTreeNode right) { if (this.value == null) { this.value = new Vector<>(); this.value.sort(new VectorComparable()); } if (child == null) { child = new Vector<>(); } this.value.add(value); this.child.add(0, left); this.child.add(1, right); if (left != null) { left.parent = this; } if (right != null) { right.parent = this; } } public BTreeNode() { parent = null; if (this.value == null) { this.value = new Vector<>(); this.value.sort(new VectorComparable()); } if (child == null) { child = new Vector<>(); } } /** * 一個關鍵塊內的查找 查找到與否都返回一個index * 返回最靠近的值的原因是為了下面的節點繼續查找 * * @param value 查找的值 * @return 不存在的情況返回最靠近的index 值 , -1 */ public int search(int value) { int hot = -1; for (int i = 0; i < this.value.size(); i++) { if (this.value.get(i) > value) { return hot; } else if (this.value.get(i) < value) { hot = i; } else { // 相等 return i; } } return this.value.size() - 1; } public int getIndexInValue(int compare) { for (int i = 0; i < this.value.size(); i++) { if (compare == value.get(i)) { return i; } } return -1; } /** * 查找當前node在父節點中的index * * @return -1 為父類不存在或是父類為null ,其他為當前節點在父節點為位置 */ public int getIndexFromParent() { if (parent == null) { return -1; } for (int i = 0; i < parent.child.size(); i++) { if (parent.child.get(i) == this) { return i; } } return -1; } public void addValue(int index, int val) { value.add(index, val); value.sort(new VectorComparable()); } public void addValue(int val) { value.add(val); value.sort(new VectorComparable()); } }
B-樹數據結構方法。
package BTree; public class BTree { private BTreeNode root; private int degree; // m階B-樹 ,階樹至少為3 /* * 私有方法 */ /** * 查找在哪個BTreeNode ,假如到了外部節點,返回該外部節點 返回的結果只有兩種 : * - 存在,返回該節點 * - 不存在,返回值應該插入的節點 * * @param val 查找的值 * @return 返回搜索結果,假如該關鍵塊不存在(到達了外部節點)就返回該關鍵快 */ private BTreeNode searchSurroundNode(int val) { BTreeNode node, hot = null; int rank; node = root; while (node != null) { rank = node.search(val); if (rank != -1 && node.value.get(rank) == val) { // 找到對應的值 return node; } else { hot = node; if (node.child.get(rank + 1) == null) { return hot; } node = node.child.get(rank + 1); } } // 到了外部節點 return hot; } private void addNodeForBtNode(BTreeNode node, int rank, int val) { node.addValue(val); if (rank != -1) { node.child.add(rank + 2, null); } else { node.child.add(0, null); } } /* * 下面為可調用的方法 */ public BTree(int degree) { this.degree = degree; } /** * 返回值所在的節點 * * @param val 插入的值 * @return 找到的話返回節點,找不到返回 null */ public BTreeNode search(int val) { BTreeNode node = searchSurroundNode(val); if (node.value.get(node.search(val)) == val) { // 該節點存在該值 return node; } return null; } /** * * 插入的值都會進入到底部節點 * @param val 插入的值 * @return 是否插入成功 */ public boolean insert(int val) { if (root == null) { root = new BTreeNode(val, null, null); return true; } //root 已經創建,插入的值最終會到達底部,然後插進去 BTreeNode node = searchSurroundNode(val); int rank = node.search(val); if (rank != -1 && node.value.get(rank) == val) { // 該節點存在該值,返回插入失敗 return false; } else { // 值將會插入該關鍵碼 addNodeForBtNode(node, rank, val); split(node); return true; } } private void split(BTreeNode node) { while (node.value.size() >= degree) { // 1.取中數 int midIndex = node.value.size() / 2; BTreeNode rightNode = new BTreeNode(); for (int i = midIndex + 1; i < node.value.size(); i++) { rightNode.addValue(node.value.remove(i)); if (i == midIndex + 1) { rightNode.child.add(node.child.remove(i)); } rightNode.child.add(node.child.remove(i)); } for (BTreeNode rn : rightNode.child) { if (rn != null) { rn.parent = rightNode; } } // 移除原節點記得移除對應它的子節點 int insertValue = node.value.remove(midIndex); if (node.parent != null) { // 存在父節點,把分裂點添加在父節點上 node.parent.addValue(insertValue); /* * 對插入的節點的子節點進行處理 * 1.得出插入點的index * 2.左邊子節點連接原node,右節點連接 rightNode */ int indexInValue = node.parent.getIndexInValue(insertValue); node.parent.child.add(indexInValue + 1, rightNode); rightNode.parent = node.parent; node = node.parent; } else { // 不存在父節點,並且當前節點溢出 root = new BTreeNode(insertValue, node, rightNode); break; } } } public boolean delete(int val) { //node 為要刪除的val所在的節點 BTreeNode node = search(val); if (node != null) { int rank = node.getIndexInValue(val); // 找到繼承結點並代替 if (node.child.get(0) != null) { //非底部節點 BTreeNode bottom = node.child.get(rank + 1); while (bottom.child.get(0) != null) { bottom = bottom.child.get(0); } node.value.set(rank, bottom.value.get(0)); bottom.value.set(0, val); node = bottom; rank = 0; } // 此時 node 一定是外部節點了(最底層) node.value.remove(rank); node.child.remove(rank + 1); // 由於刪除了某個值,所以需要從兄弟中借一個來拼湊(旋轉) // 當兄弟自己已到達下限,與父類合並成更大的節點,原來父節點所在的節點有可能-1後 // 導致又達到了下限,然後循環 solveUnderflow(node); return true; } return false; } /** * 下溢的節點 : * - 外部節點 * - 非外部節點 * * @param node 下溢的節點 */ public void solveUnderflow(BTreeNode node) { //沒有達到下溢的條件 int condition = (degree + 1) / 2; if (node.child.size() >= condition) { return; } BTreeNode parent = node.parent; if (parent == null) { //到了根節點 if (node.value.size() == 0 && node.child.get(0) != null) { root = node.child.get(0); root.parent = null; node.child.set(0, null); } return; } int rank = node.getIndexFromParent(); //旋轉 if (rank > 0 && parent.child.get(rank - 1).child.size() > condition) { //左旋轉,從左兄弟拿一個 BTreeNode ls = parent.child.get(rank - 1); node.addValue(0, parent.value.remove(rank - 1)); parent.addValue(rank - 1, ls.value.remove(ls.value.size() - 1)); /* * 被取走的節點可能存在子節點,需要放在新的位置 * 有可能上一次進行合並操作中,父節點的關鍵碼為空了, * 但是父節點還存在子節點(不為null) */ node.child.add(0, ls.child.remove(ls.child.size() - 1)); if (node.child.get(0) != null) { node.child.get(0).parent = node; } return; } else if (rank < parent.child.size() - 1 && parent.child.get(rank + 1).child.size() > condition) { //右旋轉,從右兄弟拿一個 BTreeNode rs = parent.child.get(rank + 1); node.addValue(parent.value.remove(rank)); parent.addValue(rs.value.remove(0)); node.child.add(node.child.size(), rs.child.remove(0)); if (node.child.lastElement() != null) { node.child.lastElement().parent = node; } return; } // 合並 if (rank > 0) { // 左合並 BTreeNode ls = parent.child.get(rank - 1); //父類節點轉入到左節點 ls.addValue(ls.value.size(), parent.value.remove(rank - 1)); parent.child.remove(rank); //當前節點轉入到左節點 ls.child.add(ls.child.size(), node.child.remove(0)); if (ls.child.get(ls.child.size() - 1) != null) { ls.child.get(ls.child.size() - 1).parent = ls; } // 當前節點有可能value為空,但是child不為空。 // value 為空不移動,不為空移動 while (node.value.size() != 0) { ls.addValue(node.value.remove(0)); ls.child.add(ls.child.size(), node.child.remove(0)); if (ls.child.get(ls.child.size() - 1) != null) { ls.child.get(ls.child.size() - 1).parent = ls; } } } else { //右合並,有可能 rank = 0 BTreeNode rs = parent.child.get(rank + 1); //父類節點轉入到右節點 rs.addValue(0, parent.value.remove(rank)); //父類節點斷開與當前節點的連接 parent.child.remove(rank); //當前節點轉入到右節點 rs.child.add(0, node.child.remove(0)); if (rs.child.get(0) != null) { rs.child.get(0).parent = rs; } while (node.value.size() != 0) { rs.addValue(0, node.value.remove(0)); rs.child.add(0, node.child.remove(0)); if (rs.child.get(0) != null) { rs.child.get(0).parent = rs; } } } solveUnderflow(parent); } public int height() { int h = 1; BTreeNode node = root; while (node != null) { if (node.child.get(0) != null) { h++; node = node.child.get(0); } else { break; } } return h; } }
最後是一個測試的方法。
package BTree; public class BTreeTest { public static void main(String[] args) { BTree tree = new BTree(3); tree.insert(53); tree.insert(97); tree.insert(36); tree.insert(89); tree.insert(41); tree.insert(75); tree.insert(19); tree.insert(84); tree.insert(77); tree.insert(79); tree.insert(51); // System.out.println(tree.height()); // tree.insert(23); // tree.insert(29); // tree.insert(45); // tree.insert(87); // System.out.println("-------------"); System.out.println("插入節點以後的樹的高度 : "+tree.height()); System.out.println("-------------"); // tree.delete(41); // tree.delete(75); // tree.delete(84); // tree.delete(51); tree.delete(36); tree.delete(41); System.out.println("刪除節點以後的樹的高度 : "+tree.height()); } }
參考資料
- 鄧俊輝老師的數據結構課程
數據結構(三)--- B樹(B-Tree)