1. 程式人生 > >數據結構(三)--- B樹(B-Tree)

數據結構(三)--- B樹(B-Tree)

高速緩存 .com 記得 ret 足夠 gin 行合並 add 樹和二叉樹

文章圖片代碼來自鄧俊輝老師的課件

概述

技術分享圖片

上圖就是 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)