B+樹 -- Java實現


一、B+樹定義

B+樹定義:關鍵字個數比孩子結點個數小1的樹。

除此之外B+樹還有以下的要求:

  1. B+樹包含2種類型的結點:內部結點(也稱索引結點)和葉子結點。根結點本身即可以是內部結點,也可以是葉子結點。根結點的關鍵字個數最少可以只有1個。

  2. B+樹與B樹最大的不同是內部結點不保存數據,只用於索引,所有數據(或者說記錄)都保存在葉子結點中。

  3. m階B+樹表示了內部結點最多有m-1個關鍵字(或者說內部結點最多有m個子樹),階數m同時限制了葉子結點最多存儲m-1個記錄。

  4. 內部結點中的key都按照從小到大的順序排列,對於內部結點中的一個key,左樹中的所有key都小於它,右子樹中的key都大於等於它。葉子結點中的記錄也按照key的大小排列。

  5. 每個葉子結點都存有相鄰葉子結點的指針,葉子結點本身依關鍵字的大小自小而大順序鏈接。

二、B+樹的插入操作

1)若為空樹,創建一個葉子結點,然后將記錄插入其中,此時這個葉子結點也是根結點,插入操作結束。

2)針對葉子類型結點:根據key值找到葉子結點,向這個葉子結點插入記錄。插入后,若當前結點key的個數小於等於m-1,則插入結束。否則將這個葉子結點分裂成左右兩個葉子結點,左葉子結點包含前m/2個記錄,右結點包含剩下的記錄,將第m/2+1個記錄的key進位到父結點中(父結點一定是索引類型結點),進位到父結點的key左孩子指針向左結點,右孩子指針向右結點。將當前結點的指針指向父結點,然后執行第3步。

3)針對索引類型結點:若當前結點key的個數小於等於m-1,則插入結束。否則,將這個索引類型結點分裂成兩個索引結點,左索引結點包含前(m-1)/2個key,右結點包含m-(m-1)/2個key,將第m/2個key進位到父結點中,進位到父結點的key左孩子指向左結點, 進位到父結點的key右孩子指向右結點。將當前結點的指針指向父結點,然后重復第3步。

下面是一顆5階B樹的插入過程,5階B數的結點最少2個key,最多4個key。

a)空樹中插入5

image

b)依次插入8,10,15

image

c)插入16

image

插入16后超過了關鍵字的個數限制,所以要進行分裂。在葉子結點分裂時,分裂出來的左結點2個記錄,右邊3個記錄,中間key成為索引結點中的key,分裂后當前結點指向了父結點(根結點)。結果如下圖所示。

image

當然我們還有另一種分裂方式,給左結點3個記錄,右結點2個記錄,此時索引結點中的key就變為15。

d)插入17
image

e)插入18,插入后如下圖所示

image

當前結點的關鍵字個數大於5,進行分裂。分裂成兩個結點,左結點2個記錄,右結點3個記錄,關鍵字16進位到父結點(索引類型)中,將當前結點的指針指向父結點。

image

當前結點的關鍵字個數滿足條件,插入結束

f)插入若干數據后

image

g)在上圖中插入7,結果如下圖所示

image

當前結點的關鍵字個數超過4,需要分裂。左結點2個記錄,右結點3個記錄。分裂后關鍵字7進入到父結點中,將當前結點的指針指向父結點,結果如下圖所示。

image

當前結點的關鍵字個數超過4,需要繼續分裂。左結點2個關鍵字,右結點2個關鍵字,關鍵字16進入到父結點中,將當前結點指向父結點,結果如下圖所示。

image

當前結點的關鍵字個數滿足條件,插入結束。

三、B+樹的刪除操作

如果葉子結點中沒有相應的key,則刪除失敗。否則執行下面的步驟

1)刪除葉子結點中對應的key。刪除后若結點的key的個數大於等於Math.ceil(m-1)/2 – 1,刪除操作結束,否則執行第2步。

2)若兄弟結點key有富余(大於Math.ceil(m-1)/2 – 1),向兄弟結點借一個記錄,同時用借到的key替換父結(指當前結點和兄弟結點共同的父結點)點中的key,刪除結束。否則執行第3步。

3)若兄弟結點中沒有富余的key,則當前結點和兄弟結點合並成一個新的葉子結點,並刪除父結點中的key(父結點中的這個key兩邊的孩子指針就變成了一個指針,正好指向這個新的葉子結點),將當前結點指向父結點(必為索引結點),執行第4步(第4步以后的操作和B樹就完全一樣了,主要是為了更新索引結點)。

4)若索引結點的key的個數大於等於Math.ceil(m-1)/2 – 1,則刪除操作結束。否則執行第5步

5)若兄弟結點有富余,父結點key下移,兄弟結點key上移,刪除結束。否則執行第6步

6)當前結點和兄弟結點及父結點下移key合並成一個新的結點。將當前結點指向父結點,重復第4步。

注意,通過B+樹的刪除操作后,索引結點中存在的key,不一定在葉子結點中存在對應的記錄。

下面是一顆5階B樹的刪除過程,5階B數的結點最少2個key,最多4個key。

a)初始狀態

image

b)刪除22,刪除后結果如下圖

image

刪除后葉子結點中key的個數大於等於2,刪除結束

c)刪除15,刪除后的結果如下圖所示

image

刪除后當前結點只有一個key,不滿足條件,而兄弟結點有三個key,可以從兄弟結點借一個關鍵字為9的記錄,同時更新將父結點中的關鍵字由10也變為9,刪除結束。

image

d)刪除7,刪除后的結果如下圖所示
image

當前結點關鍵字個數小於2,(左)兄弟結點中的也沒有富余的關鍵字(當前結點還有個右兄弟,不過選擇任意一個進行分析就可以了,這里我們選擇了左邊的),所以當前結點和兄弟結點合並,並刪除父結點中的key,當前結點指向父結點。

image

此時當前結點的關鍵字個數小於2,兄弟結點的關鍵字也沒有富余,所以父結點中的關鍵字下移,和兩個孩子結點合並,結果如下圖所示。

image

四、Java代碼實現

public class BTreeNode {

    public BTreeNode parent;//父節點

    /*以升序方式存儲.*/
    public List<Integer> keys;
    /*孩子*/
    public List<BTreeNode> children;

    public boolean leaf;//是否是子節點

    /*子節點中指向下一個節點.*/
    public BTreeNode next;

    public BTreeNode() {
        keys = new ArrayList<>();
        children = new ArrayList<>();
        leaf = false;
    }

    /*返回關鍵字個數*/
    public int size() {
        return keys.size();
    }

    //該節點中存儲的索引是否包含該key值,包含則返回當前索引值,否則返回小於key值的索引
    public SearchResult searchKey(Integer key) {
        int index = Collections.binarySearch(keys, key);
        if (index >= 0) {
            return new SearchResult(index, true);
        } else {
            return new SearchResult(Math.abs(index + 1), false);
        }
    }

    //keys集合是升序排序,這里做了排序的動作,可以直接添加,然后對集合重新排序
    public void addKey(Integer key) {
        SearchResult searchResult = searchKey(key);
        if (!searchResult.found) {
            List<Integer> list = new ArrayList<>(size() + 1);
            for (int i = 0; i < searchResult.index; i++) {
                list.add(keys.get(i));
            }
            list.add(key);
            for (int i = searchResult.index; i < keys.size(); i++) {
                list.add(keys.get(i));
            }
            keys = list;
        }
    }

    //從集合中移除索引
    public void removeKey(Integer key) {
        keys.remove(key);
    }

    //獲取子孩子
    public BTreeNode childAt(int index) {
        if (leaf) {
            throw new UnsupportedOperationException("Leaf node doesn't have children.");
        } else {
            return children.get(index);
        }
    }
    
    //將子孩子添加到集合末尾
    public void addChild(BTreeNode node) {
        children.add(node);
    }

    public void removeChild(int index) {
        children.remove(index);
    }

    //某個位置上的子孩子添加
    public void addChild(BTreeNode child, int index) {
        List<BTreeNode> newChildren = new ArrayList<>();
        int i = 0;
        for (; i < index; ++i) {
            newChildren.add(children.get(i));
        }
        newChildren.add(child);
        for (; i < children.size(); ++i) {
            newChildren.add(children.get(i));
        }
        children = newChildren;
    }
}

public class SearchResult {

    public int index;//索引所在集合的位置

    public boolean found;//是否找到索引

    public SearchResult() {

    }

    public SearchResult(int index, boolean found) {
        this.index = index;
        this.found = found;
    }
}

public class Result extends SearchResult {

    public BTreeNode node;//當前節點,索引值沒有找到,則為要插入的節點
    
    public int parentIndex;//當前節點在父級節點的位置
     
    public Result(BTreeNode node, int index, int parentIndex, boolean found) {
        super(index, found);
        this.node = node;
        this.parentIndex = parentIndex;
    }

    public Result(BTreeNode node, int parentIndex, SearchResult searchResult) {
        super(searchResult.index, searchResult.found);
        this.node = node;
        this.parentIndex = parentIndex;
    }
}

上面的基本定義則描述了一個節點包括其索引集合,還包括其子孩子,並且在BTreeNode中封裝了一些方法,供后續調用

public class BTree {

    private static final int DEFAULT_T = 2;
    public BTreeNode root;
    /* 根據B樹的定義,B樹的每個非根節點的關鍵字數n滿足(t - 1) <= n <= (2t - 1) */
    private int t = DEFAULT_T;
    /* 非根節點中最小的鍵值數 */
    private int minKeySize;
    /* 非根節點中最大的鍵值數 */
    private int maxKeySize;

    public BTree(int t /*傳入b樹的階數*/) {
        this();
        this.t = t;
        minKeySize = t / 2;
        maxKeySize = t - 1;
    }
}

封裝方法,找到當前索引的位置,沒有找到時,則返回索引所在的節點中集合的位置,

public Result searchLeafNode(BTreeNode node, int parentIndex, Integer key) {
        SearchResult searchResult = node.searchKey(key);
        if (node.leaf) {//子節點
            return new Result(node, parentIndex, searchResult);
        } else {
            if (searchResult.found) {
                searchResult.index++;
            }
            return searchLeafNode(node.children.get(searchResult.index), searchResult.index, key);
        }
    }

插入思路:

  1. 找到關鍵字的位置,找到該節點一定是子節點。
  2. 添加了關鍵字的節點,判斷是否滿了,滿了則進行分裂
  3. 子節點分裂時,選取中間值上升為父節點中值,但不從子節點中移除,因為子節點保存關鍵字的值,非子節點保存僅僅是索引
    public boolean insert(Integer key) {
        // 找到子節點
        Result result = searchLeafNode(root, 0, key);
        if (result.found) {//找到該節點,不操作
            return false;
        }
        BTreeNode node = result.node;
        node.addKey(key);
        //判斷節點是否滿了,滿了則進行分割
        if (isFull(node)) {
            split(node.parent, result.parentIndex, node);
        }
        return true;
    }
    
    //進行分割
    public void split(BTreeNode parentNode, int parentIndex, BTreeNode childNode) {
        //將當前節點一份為二,小的部分將放入到新節點中,自身則成為右節點
        int mid = childNode.size() / 2;
        Integer key = null;
        boolean unLeaf = childNode.children.isEmpty();
        //判斷是否為子節點,如果是子節點,索引會放入到右節點中,否則會放入到父節點中
        if (unLeaf) {
            key = childNode.keys.get(mid);
        } else {
            key = childNode.keys.remove(mid);
        }
        //分裂出左節點形成新的節點
        List<Integer> keys = new ArrayList<>();
        for (int i = 0; i < mid; i++) {
            Integer k = childNode.keys.remove(0);
            keys.add(k);
        }
        BTreeNode node = new BTreeNode();
        node.parent = parentNode;
        node.leaf = childNode.children.isEmpty();
        node.keys.addAll(keys);
        node.next = childNode;//節點下一個
        //將孩子節點部分也移動到新節點中
        if (!unLeaf) {
            mid = childNode.children.size() / 2;
            for (int i = 0; i < mid; i++) {
                BTreeNode bTreeNode = childNode.children.remove(0);
                bTreeNode.parent = node;
                node.addChild(bTreeNode);
            }
        }
        //父節點為空時,需要產生一個新節點
        if (parentNode == null) {
            root = new BTreeNode();
            root.leaf = false;
            parentNode = root;
            childNode.parent = parentNode;
            node.parent = parentNode;
            parentNode.children.add(childNode);
        }

        int index = parentNode.addKey(key);
        //前一個指針的下一個指針重新定向
        BTreeNode preNode = parentNode.children.get(parentIndex);
        preNode.next = node;
        //將節點添加到列表中
        parentNode.addChild(node, index);
        if (isFull(parentNode)) {//父節點終索引是否滿了,滿了,則繼續分裂
            split(parentNode.parent, 0, parentNode);
        }
    }
    
    private boolean isFull(BTreeNode node) {
        return node.size() > maxKeySize;
    }

刪除關鍵字思路:

  1. 找到該節點,如果是為找到,直接返回
  2. 找到該節點,出現一定是在子節點中,移除掉后,判斷子節點的索引值是否大於最小數,大於則返回,否則需要進行合並。移除的當前節點如果出現在父節點中,也需要移除。會從子節點中選擇一個節點進行補充
  3. 刪除后小於最小數,則先從兄弟節點借,如果兄弟節點借不出,則進行合並
  4. 子節點進行合並,不需要移動子孩子
  5. 父節點進行合並,需要將孩子節點移動
    //刪除節點
    public boolean delete(Integer key) {
        //找到該節點
        Result result = searchLeafNode(root, 0, key);
        if (!result.found) {//未找到
            return false;
        }
        //刪除的節點數量大於
        BTreeNode node = result.node;
        node.removeKey(key);
        if (node.keys.size() >= minKeySize) {
            if (node.parent.keys.contains(key)) {//父節點中包含該節點
                Integer min = node.keys.get(0);
                node.parent.removeKey(key);
                node.parent.addKey(min);
            }
            return true;
        }
        //刪除節點后,不滿足情況,則找兄弟節點借
        BTreeNode parent = node.parent;
        if (result.parentIndex != 0 && parent.children.get(result.parentIndex - 1).keys.size() > minKeySize) {//左節點有富余可以借
            BTreeNode left = parent.children.get(result.parentIndex - 1);
            Integer max = left.keys.remove(left.keys.size() - 1);
            //替換節點
            if (parent.keys.contains(key)) {
                parent.removeKey(key);
                parent.addKey(max);
                node.addKey(max);
            } else {
                Integer min = node.keys.get(0);
                parent.removeKey(min);
                parent.addKey(max);
                node.addKey(max);
            }
        } else if (result.parentIndex < parent.children.size() - 1 && parent.children.get(result.parentIndex - 1).keys.size() > minKeySize) {//右節點有富余可以借
            BTreeNode right = parent.children.get(result.parentIndex + 1);
            Integer min = right.keys.remove(0);
            //替換節點
            if (parent.keys.contains(key)) {
                parent.removeKey(key);
                parent.addKey(min);
                node.addKey(min);
            } else {
                Integer max = node.keys.get(node.keys.size() - 1);
                parent.removeKey(max);
                parent.addKey(min);
                node.addKey(min);
            }
        } else {
            //兄弟節點也沒有,則進行合並
            node.parent.removeKey(key);
            node.parent.removeKey(node.keys.get(0));
            union(node, result.parentIndex);
        }
        return true;
    }

    public void union(BTreeNode node, int parentIndex) {
        int ch = 0;
        if (parentIndex == 0) {//當前節點是最左節點,則只能找右節點
            ch = 1;
        } else {//否則找左節點
            ch = parentIndex - 1;
        }
        BTreeNode parent = node.parent;
        if (parent == null) {
            return;
        }
        BTreeNode kNode = parent.children.get(ch);
        for (int i = 0; i < node.size(); i++) {
            kNode.addKey(node.keys.get(i));
        }
        parent.removeChild(parentIndex);//移除節點
        //判斷上級節點
        if (parent.keys.size() < minKeySize) {
            union(parent);
        }
    }


    public void union(BTreeNode node) {
        if (node.parent == null) {
            return;
        }
        Integer min = node.keys.get(0);
        BTreeNode parent = node.parent;
        //找到當前節點的位置
        Integer index = -1;
        for (int i = parent.keys.size() - 1; i >= 0; i--) {
            if (min > parent.keys.get(i)) {
                index = i;
                break;
            }
        }
        Integer parentValue = null;
        if (index != -1) {
            parentValue = parent.keys.get(index);
        } else {//沒有找到則表示當前節點為最左節點
            parentValue = parent.keys.get(index + 1);
        }
        if (index != -1 && parent.children.get(index).keys.size() > minKeySize) {
            //判斷左節點是否富余
            BTreeNode left = parent.children.get(index);
            Integer max = left.keys.get(left.size() - 1);
            parent.keys.add(index, max);
            node.addKey(parentValue);
            node.addChild(left.children.get(left.children.size() - 1), 0);
        } else if ((index == -1 && parent.children.get(index + 2).keys.size() > minKeySize) || (index < parent.keys.size() - 1 && parent.children.get(index + 1).keys.size() > minKeySize)) {
            //判斷右節點是否富余
            BTreeNode right = parent.children.get(index + 1);
            Integer m = right.keys.get(0);
            parent.keys.add(index, m);
            node.addKey(parentValue);
            node.addChild(right.children.get(0));
        } else {
            //合並
            if (index == -1) {
                //合並到右節點
                BTreeNode right = parent.children.get(index + 2);
                Integer pa = parent.keys.remove(index + 1);
                right.addKey(pa);
                for (int i = 0; i < node.keys.size(); i++) {
                    right.addKey(node.keys.get(i));
                }
                List<BTreeNode> bTreeNodes = new ArrayList<>();
                for (int i = 0; i < node.children.size(); i++) {
                    bTreeNodes.add(node.children.get(i));
                }
                for (int i = 0; i < right.children.size(); i++) {
                    bTreeNodes.add(right.children.get(i));
                }
                right.children = bTreeNodes;
                parent.children.remove(index + 1);//移除該節點
                if (parent.keys.isEmpty()) {//節點為空
                    root = right;
                    return;
                }
            } else {
                //合並到左節點
                //找到父級節點下沉,並將該節點所有添加到左節點中
                BTreeNode left = parent.children.get(index);
                Integer max = parent.keys.remove(index.intValue());
                left.addKey(max);
                for (int i = 0; i < node.keys.size(); i++) {
                    left.addKey(node.keys.get(i));
                }
                for (int i = 0; i < node.children.size(); i++) {
                    left.children.add(node.children.get(i));
                }
                parent.children.remove(index + 1);//移除該節點
                if (parent.keys.isEmpty()) {//節點為空
                    root = left;
                    return;
                }
            }
        }
        //判斷上級節點
        if (parent.keys.size() < minKeySize) {
            union(parent);
        }
    }

打印輸出:

private void outPut(BTreeNode node, int index) {
        if (node.leaf) {
            List<Integer> kes = node.keys;
            System.out.println("葉子節點,層級:" + index + ",keys:" + kes);
        } else {
            List<Integer> kes = node.keys;
            System.out.println("層級:" + index + ",keys:" + kes);
            for (int i = 0; i < node.children.size(); i++) {
                outPut(node.children.get(i), index + 1);
            }
        }
    }

    public static void main(String[] args) {
        BTree tree = new BTree(5);
        tree.insert(5);
        tree.insert(8);
        tree.insert(10);
        tree.insert(15);
        tree.insert(16);
        tree.insert(17);
        tree.insert(6);
        tree.insert(9);
        tree.insert(18);
        tree.insert(19);
        tree.insert(20);
        tree.insert(21);
        tree.insert(22);
        tree.insert(7);
        tree.outPut(tree.root, 0);

        System.out.println("---------------------------------------------");

        tree.delete(22);
        tree.delete(15);
        tree.outPut(tree.root, 0);
        System.out.println("---------------------------------------------");
        tree.delete(7);
        tree.outPut(tree.root, 0);
        System.out.println("---------------------------------------------");
    }

最后的結果:

層級:0,keys:[16]
層級:1,keys:[7, 10]
葉子節點,層級:2,keys:[5, 6]
葉子節點,層級:2,keys:[7, 8, 9]
葉子節點,層級:2,keys:[10, 15]
層級:1,keys:[18, 20]
葉子節點,層級:2,keys:[16, 17]
葉子節點,層級:2,keys:[18, 19]
葉子節點,層級:2,keys:[20, 21, 22]
---------------------------------------------
層級:0,keys:[16]
層級:1,keys:[7, 9]
葉子節點,層級:2,keys:[5, 6]
葉子節點,層級:2,keys:[7, 8]
葉子節點,層級:2,keys:[9, 10]
層級:1,keys:[18, 20]
葉子節點,層級:2,keys:[16, 17]
葉子節點,層級:2,keys:[18, 19]
葉子節點,層級:2,keys:[20, 21]
---------------------------------------------
層級:0,keys:[9, 16, 18, 20]
葉子節點,層級:1,keys:[5, 6, 8]
葉子節點,層級:1,keys:[9, 10]
葉子節點,層級:1,keys:[16, 17]
葉子節點,層級:1,keys:[18, 19]
葉子節點,層級:1,keys:[20, 21]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM