【JDK17】BTree看这一篇就够了!BTree的添加、删除key的实现原理及代码实现

B树是对m叉树的一种规则约束,它的定义如下:

  1. 每一个节点最多有 m 个子节点
  2. 每一个非叶子节点(除根节点)最少有 ⌈m/2⌉ 个子节点
  3. 如果根节点不是叶子节点,那么它至少有两个子节点
  4. 有 k 个子节点的非叶子节点拥有 k − 1 个键
  5. 所有的叶子节点都在同一层
  6. 除根节点外的其他节点,key的数目至少要有(m-1)/2个

假设m=5,则除根节点外的其他节点,至少要有(5-1)/2=2个key,最多有5-1=4个key

1、添加key

如下图,蓝色格子代表预留出来的一个容量,实际存储时,只有前4个格子可以存储key。蓝色线条也是预留出来的一个孩子指针,实际只有前5个孩子指针会指向子节点。

有了以上概念,我们尝试向BTree添加key,

首先第一步就是要找到key要添加到哪个节点,我们可以通过二分查找快速定位到“10”落入当前节点node的哪个区间。

    /**
     * 二分查找数组中的key
     * @param arr
     * @param left
     * @param right
     * @param key
     * @return 如果找到,就将全局变量isFind改为true并且返回key所在的索引;如果没找到,将全局变量isFind设置为false,并且返回key落入哪个区间section的孩子索引
     * @param <T>
     */
    private <T extends Comparable<T>> int binarySearchIndexOrSection(T[] arr, int left, int right, T key) {
        int mid = (left + right) / 2;
        int cmp = key.compareTo(arr[mid]);

        if (cmp == 0) {
            //当前节点找到key,将isFind置为true,并返回key所在的索引
            isFind = true;
            return mid;
        } else if (left >= right) {
            //最后一次查找还是找不到key,返回该key落入哪个区间section
            isFind = false;
            if (cmp < 0) {
                //比当前mid要小,说明key落入mid的左边,childIndex应该是mid
                return mid;
            }else {
                //比当前mid要小,说明key落入mid的右边,childIndex应该是mid+1
                return mid + 1;
            }
        } else if (cmp < 0) {
            return binarySearchIndexOrSection(arr, left, mid - 1, key);
        }else {
            return binarySearchIndexOrSection(arr, mid + 1, right, key);
        }
    }

通过上面的方法,我们就找到了key要添加到哪个节点,接下来将key添加到该节点的keys数组内会有几种情况需要区分:

第①种情况:keys数组本身有空余的位置,如上图,这时候就能直接按顺序将key插入keys数组。

第②种情况:keys数组没有多余的位置,这时候需要将key先插入到keys数组中,然后进行分裂操作。(注意分裂操作不仅仅会在叶子节点,也有可能发生在非叶子节点

我们假设要分裂的是非叶子节点(这种情况只有叶子节点分裂后导致父节点的不平衡才会出现),这里假设“B”这个叶子节点分裂后,要将“8”添加到父节点,导致父节点不平衡

叶子节点不平衡,间接导致父节点也不平衡,这里注意要把孩子节点顺位后移

此时该非叶子节点也不平衡,需要新增一个父节点和一个右节点,中间的key移到新增的父节点,中间靠右的全部key移到新增的右结点

然后一定要记得处理孩子节点的归属问题,右节点在复制key的同时,也要把孩子节点也带过去

此时就完成了对非叶子节点的分裂操作。

对于叶子节点的分裂操作其实是类似的,只是可以把处理孩子节点的归属问题环节省略不处理,因为叶子节点的孩子全是null,处不处理孩子节点都一样。

    /**
     * 添加key时,keys数组已满,对节点进行分裂操作
     * @return 返回分裂后产生的rightSideNode
     */
    public Node<K,V> split(){
        //获取中间索引
        int midIndex = keyNum / 2;

        Node<K, V> rightSideNode = new Node<>(m, keyType, valueType);

        //处理父节点
        if (this.parent == null) {
            Node<K, V> parentNode = new Node<>(m, keyType, valueType);
            parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
            parentNode.getChildren()[0] = this;
            parentNode.getChildren()[1] = rightSideNode;
            parentNode.setLeaf(false);
            //将当前和rightSide节点的父节点设置为parentNode
            this.parent = parentNode;
            rightSideNode.parent = parentNode;

        }else {
            //如果已经有父节点,先判断当前节点是父节点的第几个孩子
            Node<K, V> parentNode = this.parent;
            int childIndex = predicateWhichChild(this);
            //先判断父节点有没有满key
            if (parentNode.keyNum == m - 1) {
                //父节点满key,先给父节点插入keys[midIndex]、values[midIndex],然后处理parentNode与子节点的关系
                parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
                insertChildren(rightSideNode, parentNode, childIndex + 1);
                //分裂父节点
                Node<K, V> parentRightSideNode = parentNode.split();
                //处理parentRightSideNode的子节点关系(本质是把parentNode的key连带着指针复制到parentRightSideNode的指针域)
                copyArray(parentNode.getChildren(), midIndex + 1, parentRightSideNode.getChildren(), 0, m - midIndex);

                //将parentNode的孩子复制到parentRightSideNode的孩子后,重置parentRightSideNode孩子的parent指针
                resetChildrenBelong(parentRightSideNode);
                //将parentRightSideNode设置成非叶子节点
                parentRightSideNode.setLeaf(false);
            }else {
                //父节点没满key,调用insertKeyAndValue直接将mid添加到父节点
                parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
                insertChildren(rightSideNode, parentNode, childIndex + 1);
                resetChildrenBelong(parentNode);

            }
        }

        removeKeyAndValue(midIndex);
        //将当前节点keys数组里,mid后面的元素和传进来的key复制到rightSideNode的keys数组里,[1,2,4,5,null]
        copyArray(keys, midIndex + 1, rightSideNode.keys, 0, m - 1 - midIndex);
        copyArray(values, midIndex + 1, rightSideNode.values, 0, m - 1 - midIndex);
        keyNum = keyNum - (m - 1 - midIndex);
        rightSideNode.keyNum = rightSideNode.keyNum + (m - 1 - midIndex);

        return rightSideNode;

    }

熟悉了对BTree的分裂操作后,我们就可以整合出对BTree的添加操作代码了

    /**
     * 添加节点
     * @param key
     * @param value
     */
    public void put(K key, V value) {
        if (root == null) {
            root = new Node<>(m, keyType, valueType);
            root.insertKeyAndValue(key, value);
            return;
        }
        //先查找key,如果能直接找到key,就只需要修改key对应的value值
        Node<K, V> node = root;
        int index;
        while (node != null) {
            //如果key能在当前节点的keys中找到,index就是key的索引;如果没找到,index就是key落入keys的哪个区间(也就是能直接指示应该找哪个孩子)
            index = binarySearchIndexOrSection(node.getKeys(), 0, node.getKeyNum() - 1, key);
            if (isFind) {
                //当前节点找到key,直接更新key value
                node.updateValueByKey(index, value);
                return;
            }else {
                //没找到,判断当前是不是叶子节点
                if (!node.isLeaf()) {
                    //不是叶子节点,就继续找区间内的孩子
                    node = node.getChildren()[index];
                }else {
                    //是叶子节点,而且该叶子节点还是没有找到key,就要进行添加操作
                    node.insertKeyAndValue(key,value);
                    break;
                }
            }
        }
        //出循环说明走的是添加key的流程,需要检查该叶子节点是否平衡
        if (!node.isBalance()) {
            //不平衡,则需要进行分裂操作
            node.split();
            //分裂可能会产生新的父节点,需要将root重新指向正确的根
            if (root.getParent() != null) {
                root = root.getParent();
            }
        }

    }

---------------------------------------------------------------------------------------------------------------------------------

2、删除key

删除操作看似复杂,实际上操作上只是添加操作的逆操作。添加操作需要分裂节点,删除操作则需要合并节点。

BTree删除key时,这个key所在的节点有可能是在叶子节点,也有可能是在非叶子节点,但其实最终都会落到删除叶子节点这种情况里。类似于AVL树,删除非叶子节点,就会转而去删除它的前驱或者后继节点(本文找后继节点作为代替),所以我们对BTree删除的第一步就是要判断删除key对应的节点是否是非叶子节点,如果删的是非叶子节点就要去找它的后继节点。

2.1、查找后继节点

如下图,删除的key所处的节点就是非叶子节点,需要找它的后继节点(右子树最左边的孩子中最左边的key)

找到后继key后,将后继key覆盖原本要删除的key,

此时就将删除非叶子节点的情况转化成了删除叶子节点的key的情况。

2.2、删除叶子节点的key

接下来是删除key的几种情况:

第①种情况:keys删除key后,该叶子节点仍然平衡(大于等于最小key数),直接删除key,如下图,直接删除“12”就行。

第②种情况:删除node上的key后,如果node是root(root是叶子节点),不管根节点有没有平衡,都直接删除key就行。

第③种情况:删除node上的key后,节点不平衡,但兄弟节点有多余的key可以借给node,如下图,删除“14”后,它的右兄弟有3个key,可以借给node,

类似于AVL树中旋转的操作,如果兄弟是node的右兄弟,则需要“左旋”,将“16”左旋至node(补充node的key含量),将“18”左旋至父节点,(如果是左兄弟,则需要“右旋”)

第④种情况:删除node上的key后,节点不平衡,兄弟节点也没有多余的key借给node,如下图的情况

将父节点被node和兄弟节点sibling夹着的key移到node,再把sibling的全部key移到node,

然后将父节点的sibling右边的全部孩子节点左移(相当于移除父节点对sibling的引用关系),

此时记得要检查父节点的key借给node后,是否会因此变得不平衡,如果平衡就可以结束方法了,如果像上图一样不平衡,就需要递归父节点进行调整(即父节点可能跳到②③④情况中的任意一种,有可能父节点的兄弟节点能借,也有可能父节点的兄弟节点不能借需要继续合并,等等情况)

如上图这样,将node指向它的父节点,继续递归调整,

显然,现在的node的兄弟节点也没有多余的key可以借给node,所以就走到了第④种情况需要继续合并,

由于node不是叶子节点,所以合并后需要解决孩子的归属问题,只需要顺延leftNode的孩子节点接管rightNode的孩子即可,

此时,只需要将root重新指向正确的根节点,然后将leftNode的parent指针置空即可(这里的情况leftNode是合并后正确的根节点)

此时原本的root,和rightNode就会因为没有引用指向这些节点会被GC自动垃圾回收,

删除key后,重新平衡BTree,

    /**
     * 重新平衡B树
     * @param node 删除key后对应的叶子节点
     */
    private void balanceTree(Node<K, V> node) {
        Node<K, V> parentNode = node.getParent();
        Node<K, V> siblingNode = getMoreChildSibling(node); //兄弟节点
        //判断兄弟节点是否有多余的key可外借
        if (siblingNode.canLendKey()) {
            //兄弟可外借key
            //先判断兄弟节点是左兄弟还是右兄弟
            if (isLeftSibling(siblingNode)) {
                //sibling是node的左兄弟
                node.insertKeyAndValue(parentNode.getKeys()[childIndex - 1], parentNode.getValues()[childIndex - 1]);
                if (siblingNode.getChildNum() != 0) {
                    node.insertChildren(siblingNode.getChildren()[siblingNode.getChildNum() - 1], node, 0);
                    siblingNode.removeChildLeftShiftByIndex(siblingNode.getChildNum() - 1);
                }
                parentNode.getKeys()[childIndex - 1] = siblingNode.getKeys()[siblingNode.getKeyNum() - 1];
                parentNode.getValues()[childIndex - 1] = siblingNode.getValues()[siblingNode.getKeyNum() - 1];
                siblingNode.removeKeyAndValueLeftShiftByIndex(siblingNode.getKeyNum() - 1);

            }else {
                //sibling是node的右兄弟
                node.insertKeyAndValue(parentNode.getKeys()[childIndex], parentNode.getValues()[childIndex]);
                node.insertChildren(siblingNode.getChildren()[0], node, node.getChildNum());
                parentNode.getKeys()[childIndex] = siblingNode.getKeys()[0];
                parentNode.getValues()[childIndex] = siblingNode.getValues()[0];
                siblingNode.removeKeyAndValueLeftShiftByIndex(0);
                siblingNode.removeChildLeftShiftByIndex(0);
            }
        }else {
            //兄弟没的借

            //把父节点的key借给node,然后node节点与兄弟节点合并
            if (isLeftSibling(siblingNode)) {
                mergeNode(siblingNode, node, childIndex - 1);
            }else {
                mergeNode(node, siblingNode, childIndex);
            }

            //合并后,要检查父节点是否平衡
            if (!parentNode.isBalance()) {
                //父节点不平衡
                //如果父节点不平衡的是root,就不用再作处理
                if (parentNode == root) {
                    //如果根节点keyNum为0,说明原根节点的最后一个key都拿去给子节点用于合并了,此时root必定只剩下一个孩子,该孩子就是新的root
                    if (root.getKeyNum() == 0) {
                        root = root.getChildren()[0];
                    }
                    return;
                }
                balanceTree(parentNode);
            }

        }
    }

至此,BTree最主要的两个添加和删除就完成了。

3、实现代码

最后贴一个BTree的完整代码实现:

3.1、节点类

public class Node<K extends Comparable<K>,V> {

    private K[] keys;
    private V[] values;
    private Node<K,V>[] children; //孩子节点数组
    private int m; //树的阶数
    private boolean isLeaf = true; //标识是否为叶子节点
    private int keyNum; //key的数量
    private final int minKeyNum; //最少子节点数
    private Node<K,V> parent; //父节点
    private Class<K> keyType; //key类型
    private Class<V> valueType; //value类型

    /**
     * 构造器
     * @param m 树的阶数
     */
    public Node(int m,Class<K> keyType,Class<V> valueType) {
        this.keyType = keyType;
        this.valueType = valueType;
        keys = (K[]) Array.newInstance(keyType, m); //可存m-1个key,容量为m是为了方便插入操作
        values = (V[]) Array.newInstance(valueType, m);
        children = (Node<K, V>[]) Array.newInstance(Node.class, m + 1); //多加一个容量,方便插入操作
        this.m = m;
        this.minKeyNum = (int)Math.ceil((double) m/2); //计算最少子节点数
    }

    public boolean isLeaf() {
        return isLeaf;
    }

    public void setLeaf(boolean leaf) {
        isLeaf = leaf;
    }

    public void setKeyNum(int keyNum) {
        this.keyNum = keyNum;
    }

    public Node<K, V> getParent() {
        return parent;
    }

    public int getKeyNum() {
        return keyNum;
    }

    public K[] getKeys() {
        return keys;
    }

    public V[] getValues() {
        return values;
    }

    public Node<K, V>[] getChildren() {
        return children;
    }

    /**
     * 根据key值,按顺序把key和value插入到对应位置
     * 调用此方法前,要保证keys数组有空位
     * @param key
     * @param value
     */
    public void insertKeyAndValue(K key, V value) {
        int cmp = 0;
        //如果keys数组全为null,直接将key,value赋值到第0个位置
        if (keys[0] == null) {
            keys[0] = key;
            values[0] = value;
            keyNum++;
            return;
        }
        //keys数组有值
        int count = 0;
        for (int i = 0; i < keys.length; i++) {
            if (keys[i] != null) {
                count++;
                cmp = key.compareTo(keys[i]);
                //传进来的key比keys[i]小,将i后面的所有元素后移,然后插入
                if (cmp < 0) {
                    insertByIndex(keys, key, i);
                    insertByIndex(values, value, i);
                    keyNum++;
                    break;
                }
            }
        }
        if (count == keyNum) {
            keys[count] = key;
            values[count] = value;
            keyNum++;
        }

    }

    /**
     * 根据索引向目标数组插入元素
     * @param targetArray 目标数组
     * @param value 要插入的值
     * @param index 指定索引
     */
    private void insertByIndex(Object[] targetArray,Object value,int index) {
        int last = keyNum - 1; //last指向数组的最后一个元素
        //将index(包含index)的元素全部后移
        while (last >= index) {
            targetArray[last + 1] = targetArray[last];
            last--;
        }
        //给index赋值
        targetArray[index] = value;
    }

    /**
     * 添加key时,keys数组已满,对节点进行分裂操作
     * @return 返回分裂后产生的rightSideNode
     */
    public Node<K,V> split(){
        //获取中间索引
        int midIndex = keyNum / 2;

        Node<K, V> rightSideNode = new Node<>(m, keyType, valueType);

        //处理父节点
        if (this.parent == null) {
            Node<K, V> parentNode = new Node<>(m, keyType, valueType);
            parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
            parentNode.getChildren()[0] = this;
            parentNode.getChildren()[1] = rightSideNode;
            parentNode.setLeaf(false);
            //将当前和rightSide节点的父节点设置为parentNode
            this.parent = parentNode;
            rightSideNode.parent = parentNode;

        }else {
            //如果已经有父节点,先判断当前节点是父节点的第几个孩子
            Node<K, V> parentNode = this.parent;
            //判断当前节点是父节点的第几个孩子
            int childIndex = predicateWhichChild(this);
            //先判断父节点有没有满key
            if (parentNode.keyNum == m - 1) {
                //父节点满key,先给父节点插入keys[midIndex]、values[midIndex],然后处理parentNode与子节点的关系
                parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
                insertChildren(rightSideNode, parentNode, childIndex + 1);
                //分裂父节点
                Node<K, V> parentRightSideNode = parentNode.split();
                //处理parentRightSideNode的子节点关系(本质是把parentNode的key连带着指针复制到parentRightSideNode的指针域)
                copyArray(parentNode.getChildren(), midIndex + 1, parentRightSideNode.getChildren(), 0, m - midIndex);

                //将parentNode的孩子复制到parentRightSideNode的孩子后,重置parentRightSideNode孩子的parent指针
                resetChildrenBelong(parentRightSideNode);
                //将parentRightSideNode设置成非叶子节点
                parentRightSideNode.setLeaf(false);
            }else {
                //父节点没满key,调用insertKeyAndValue直接将mid添加到父节点
                parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
                insertChildren(rightSideNode, parentNode, childIndex + 1);
                resetChildrenBelong(parentNode);

            }
        }

        removeKeyAndValue(midIndex);
        //将当前节点keys数组里,mid后面的元素和传进来的key复制到rightSideNode的keys数组里,[1,2,4,5,null]
        copyArray(keys, midIndex + 1, rightSideNode.keys, 0, m - 1 - midIndex);
        copyArray(values, midIndex + 1, rightSideNode.values, 0, m - 1 - midIndex);
        keyNum = keyNum - (m - 1 - midIndex);
        rightSideNode.keyNum = rightSideNode.keyNum + (m - 1 - midIndex);

        return rightSideNode;

    }

    /**
     * 将孩子插入到指定索引位置
     * @param childNode 孩子节点
     * @param parentNode 父节点
     * @param index 插入的索引
     */
    public void insertChildren(Node<K,V> childNode,Node<K,V> parentNode,int index){
        //先找最后一个子节点
        Node<K, V>[] parentChildren = parentNode.getChildren();
        int last = parentChildren.length - 1;
        while (last >= 0) {
            if (parentChildren[last] != null) {
                break;
            }
            last--;
        }
        if (last >= 0) {
            //将index(包含index)索引右边的所有孩子节点右移
            while (last >= index) {
                parentChildren[last + 1] = parentChildren[last];
                last--;
            }
            parentChildren[index] = childNode;
        }



    }

    /**
     * 判断节点是父节点的第几个孩子
     * 调用此方法前,必须保证传入的节点有父节点
     * @param node
     * @return 找到则返回这个节点在父节点中孩子的索引位置,没找到则返回-1
     */
    private int predicateWhichChild(Node<K,V> node){
        Node<K, V> parentNode = node.parent;
        for (int i = 0; i < parentNode.getChildren().length; i++) {
            if (node == parentNode.getChildren()[i]) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 移除当前节点keys、values数组中对应索引的元素
     * @param index
     */
    private void removeKeyAndValue(int index){
        //直接把index上的值置空即可
        keys[index] = null;
        values[index] = null;
        keyNum--;
    }

    /**
     * 复制数组到另一个数组(会将原数组元素取出)
     * @param srcArray 源数组
     * @param srcStart 源数组复制的起始位置
     * @param targetArray 目标数组
     * @param targetStart 目标数组粘贴的起始位置
     * @param length 要复制的数组元素个数
     * @param <T> 数组类型
     */
    public <T> void copyArray(T[] srcArray, int srcStart, T[] targetArray, int targetStart, int length) {
        for (int i = 0; i < length; i++) {
            targetArray[targetStart++] = srcArray[srcStart];
            srcArray[srcStart] = null;
            srcStart++;
        }
    }

    /**
     * 根据索引,更新value值
     * @param value
     */
    public void updateValueByKey(int index, V value) {
        values[index] = value;
    }

    /**
     * 获取当前节点的孩子数目
     * @return
     */
    public int getChildNum(){
        int childNum = 0;
        for (int i = 0; i < children.length; i++) {
            if (children[i] != null) {
                childNum++;
            }
        }
        return childNum;
    }

    /**
     * 重置孩子节点的归属,使其parent指针指向正确的父节点
     * @param node
     */
    private void resetChildrenBelong(Node<K, V> node) {
        Node<K, V>[] nodeChildren = node.getChildren();

        for (int i = 0; i < node.getChildNum(); i++) {
            //将该节点的每一个孩子的parent指针都重新指向node
            nodeChildren[i].parent = node;
        }
    }

    /**
     * 根据index删除key value并将index右边的key value左移
     * @param index
     */
    public void removeKeyAndValueLeftShiftByIndex(int index){
        int start = index;
        while (start < keyNum - 1) {
            keys[start] = keys[start + 1];
            values[start] = values[start + 1];
            start++;
        }
        keys[start] = null;
        values[start] = null;
        keyNum--;
    }

    /**
     * 根据index删除孩子,并将index右边的孩子全部左移
     * @param index
     */
    public void removeChildLeftShiftByIndex(int index){
        int start = index;
        while (start < getChildNum() - 1) {
            children[start] = children[start + 1];
            start++;
        }
        children[start] = null;
    }

    /**
     * 判断当前节点是否平衡(只检查最小需求节点)
     * @return
     */
    public boolean isBalance(){
        return keyNum >= ((m - 1) / 2) && keyNum <= m - 1;
    }

    /**
     * 判断当前节点是否有多余的key可外借
     * @return
     */
    public boolean canLendKey(){
        return keyNum > ((m - 1) / 2);
    }
}

3.2、BTree类

public class BTree<K extends Comparable<K>,V> {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入B树的阶数:");
        int rank = scanner.nextInt();
        System.out.println("请输入要插入的个数:");
        int num = scanner.nextInt();
        BTree<Integer, Integer> tree = new BTree<>(rank, Integer.class, Integer.class);
        System.out.println("当前树的阶数是:" + rank);
        System.out.println("B树插入" + num + "个数据");
        System.out.println("------------------------------------------------------------");
        for (int i = 1; i <= num; i++) {
            tree.put(i, i * 10);
        }
        tree.printTree();
        while (true) {
            System.out.println("------------------------------------------------------------");
            System.out.println("输入1展示BTree");
            System.out.println("输入2添加节点");
            System.out.println("输入3删除节点");
            System.out.println("输入4查看节点");

            int order = scanner.nextInt();
            switch (order) {
                case 1 -> tree.printTree();
                case 2 -> {
                    System.out.println("添加请输入key:");
                    int key = scanner.nextInt();
                    System.out.println("添加请输入value:");
                    int value = scanner.nextInt();
                    tree.put(key, value);
                    System.out.println("添加节点成功!");
                }
                case 3 -> {
                    System.out.println("删除请输入key:");
                    int key = scanner.nextInt();
                    tree.remove(key);

                }
                case 4 -> {
                    System.out.println("查找请输入key:");
                    int key = scanner.nextInt();
                    Integer value = tree.get(key);
                    if (value != null) {
                        System.out.println("key [" + key + "] 对应的value是:[" + value + "]");
                    }

                }
            }
        }

    }

    private Node<K,V> root;
    private Class<K> keyType;
    private Class<V> valueType;
    private int m; //B树的阶
    private int count = 0; //用于遍历节点时计数
    private boolean isFind; //用于二分查找时判断是否找到key
    private int keyIndex; //调用searchNodeByKey后,会记录key在keys中的位置
    private int childIndex; //调用getMoreChildSibling后,会记录node所指向的叶子节点是父节点的第几个孩子

    public Node<K, V> getRoot() {
        return root;
    }

    /**
     * B树构造器
     * @param m 树的阶数
     * @param keyType key类型
     * @param valueType value类型
     */
    public BTree(int m, Class<K> keyType, Class<V> valueType) {
        this.m = m;
        this.keyType = keyType;
        this.valueType = valueType;
    }

    /**
     * 添加节点
     * @param key
     * @param value
     */
    public void put(K key, V value) {
        if (root == null) {
            root = new Node<>(m, keyType, valueType);
            root.insertKeyAndValue(key, value);
            return;
        }
        //先查找key,如果能直接找到key,就只需要修改key对应的value值
        Node<K, V> node = root;
        int index;
        while (node != null) {
            //如果key能在当前节点的keys中找到,index就是key的索引;如果没找到,index就是key落入keys的哪个区间(也就是能直接指示应该找哪个孩子)
            index = binarySearchIndexOrSection(node.getKeys(), 0, node.getKeyNum() - 1, key);
            if (isFind) {
                //当前节点找到key,直接更新key value
                node.updateValueByKey(index, value);
                return;
            }else {
                //没找到,判断当前是不是叶子节点
                if (!node.isLeaf()) {
                    //不是叶子节点,就继续找区间内的孩子
                    node = node.getChildren()[index];
                }else {
                    //是叶子节点,而且该叶子节点还是没有找到key,就要进行添加操作
                    node.insertKeyAndValue(key,value);
                    break;
                }
            }
        }
        //出循环说明走的是添加key的流程,需要检查该叶子节点是否平衡
        if (!node.isBalance()) {
            //不平衡,则需要进行分裂操作
            node.split();
            //分裂可能会产生新的父节点,需要将root重新指向正确的根
            if (root.getParent() != null) {
                root = root.getParent();
            }
        }

    }

    /**
     * 删除节点
     * @param key
     * @return 返回删除节点的value值
     */
    public V remove(K key) {
        //先找到要删除的key对应的节点
        Node<K, V> removeNode = searchNodeByKey(key);
        if (removeNode == null) {
            //整棵树都没有这个key,直接返回null
            System.out.println("没有找到key,删除失败!");
            return null;
        }
        //先把key对应的value取出来
        V oldValue = removeNode.getValues()[keyIndex];
        //判断当前要删除的key所在的节点是否是叶子节点,如果不是叶子节点,就找它的后继key进行替换
        //后继节点一定是key右边的那个指针所指向的孩子节点,后继key一定是后继节点的第一个key
        Node<K, V> node = removeNode; //node用于辅助找后继key
        while (!node.isLeaf()) {
            node = node.getChildren()[keyIndex + 1];
            //记得更新keyIndex,因为后继key永远是后继节点的第一个key,所以要让keyIndex = 0
            keyIndex = 0;
        }
        removeNode.getKeys()[keyIndex] = node.getKeys()[0];
        removeNode.getValues()[keyIndex] = node.getValues()[0];
        //退出循环,node必定指向叶子节点
        //删除叶子节点keyIndex上的key value(注意要把keyIndex右边的key左移)
        node.removeKeyAndValueLeftShiftByIndex(keyIndex);
        //如果node指向root,说明root是叶子节点,直接返回oldValue
        if (node == root) {
            return oldValue;
        }

        //删除后检查叶子节点是否平衡
        if (!node.isBalance()) {
            //当前叶子节点不平衡,调用balanceTree重新调整B树至平衡
            balanceTree(node);
        }
        System.out.println("删除[" + key + "]成功!");
        return oldValue;

    }

    /**
     * 重新平衡B树
     * @param node 删除key后对应的叶子节点
     */
    private void balanceTree(Node<K, V> node) {
        Node<K, V> parentNode = node.getParent();
        Node<K, V> siblingNode = getMoreChildSibling(node); //兄弟节点
        //判断兄弟节点是否有多余的key可外借
        if (siblingNode.canLendKey()) {
            //兄弟可外借key
            //先判断兄弟节点是左兄弟还是右兄弟
            if (isLeftSibling(siblingNode)) {
                //sibling是node的左兄弟
                node.insertKeyAndValue(parentNode.getKeys()[childIndex - 1], parentNode.getValues()[childIndex - 1]);
                if (siblingNode.getChildNum() != 0) {
                    node.insertChildren(siblingNode.getChildren()[siblingNode.getChildNum() - 1], node, 0);
                    siblingNode.removeChildLeftShiftByIndex(siblingNode.getChildNum() - 1);
                }
                parentNode.getKeys()[childIndex - 1] = siblingNode.getKeys()[siblingNode.getKeyNum() - 1];
                parentNode.getValues()[childIndex - 1] = siblingNode.getValues()[siblingNode.getKeyNum() - 1];
                siblingNode.removeKeyAndValueLeftShiftByIndex(siblingNode.getKeyNum() - 1);

            }else {
                //sibling是node的右兄弟
                node.insertKeyAndValue(parentNode.getKeys()[childIndex], parentNode.getValues()[childIndex]);
                node.insertChildren(siblingNode.getChildren()[0], node, node.getChildNum());
                parentNode.getKeys()[childIndex] = siblingNode.getKeys()[0];
                parentNode.getValues()[childIndex] = siblingNode.getValues()[0];
                siblingNode.removeKeyAndValueLeftShiftByIndex(0);
                siblingNode.removeChildLeftShiftByIndex(0);
            }
        }else {
            //兄弟没的借

            //把父节点的key借给node,然后node节点与兄弟节点合并
            if (isLeftSibling(siblingNode)) {
                mergeNode(siblingNode, node, childIndex - 1);
            }else {
                mergeNode(node, siblingNode, childIndex);
            }

            //合并后,要检查父节点是否平衡
            if (!parentNode.isBalance()) {
                //父节点不平衡
                //如果父节点不平衡的是root,就不用再作处理
                if (parentNode == root) {
                    //如果根节点keyNum为0,说明原根节点的最后一个key都拿去给子节点用于合并了,此时root必定只剩下一个孩子,该孩子就是新的root
                    if (root.getKeyNum() == 0) {
                        root = root.getChildren()[0];
                    }
                    return;
                }
                balanceTree(parentNode);
            }

        }
    }

    /**
     * 把key value作为中间媒介,将rightNode合并到leftNode
     * @param leftNode 左节点
     * @param rightNode 右节点
     * @param parentKeyIndex 父节点的key索引(leftNode跟rightNode夹着的key)
     */
    private void mergeNode(Node<K,V> leftNode,Node<K,V> rightNode,int parentKeyIndex){
        Node<K, V> parentNode = leftNode.getParent();
        int length = rightNode.getKeyNum(); //要复制的长度
        leftNode.insertKeyAndValue(parentNode.getKeys()[parentKeyIndex], parentNode.getValues()[parentKeyIndex]);

        leftNode.copyArray(rightNode.getKeys(), 0, leftNode.getKeys(), leftNode.getKeyNum(), length);
        leftNode.copyArray(rightNode.getValues(), 0, leftNode.getValues(), leftNode.getKeyNum(), length);
        //复制完后记得要更新keyNum
        leftNode.setKeyNum(leftNode.getKeyNum() + length);
        //记得还要处理leftNode的children数组
        leftNode.copyArray(rightNode.getChildren(), 0, leftNode.getChildren(), leftNode.getChildNum(), rightNode.getChildNum());

        //把parentNode对rightNode的孩子索引删除,并将右边的孩子全部左移
        parentNode.removeChildLeftShiftByIndex(parentKeyIndex + 1);
        //把parentNode上parentKeyIndex上的key value删除
        parentNode.removeKeyAndValueLeftShiftByIndex(parentKeyIndex);
    }

    /**
     * 返回node节点的所有相邻兄弟节点中,key较多的兄弟节点,左右两兄弟keyNum相等就优先返回右兄弟
     * 调用此函数前,必须保证node有父节点
     * 返回值不可能返回null,因为node必定有兄弟节点
     * @param node
     * @return
     */
    private Node<K,V> getMoreChildSibling(Node<K,V> node){
        Node<K, V> parentNode = node.getParent();
        //遍历父节点的children数组,找到node是parentNode的第几个孩子
        int childIndex = 0;
        for (Node<K, V> child : parentNode.getChildren()) {
            if (node == child) {
                break;
            }
            childIndex++;
        }
        this.childIndex = childIndex; //顺便将childIndex记录到类成员变量,方便后续直接拿来使用
        Node<K, V> leftChild = childIndex == 0 ? null : parentNode.getChildren()[childIndex - 1];
        Node<K, V> rightChild = parentNode.getChildren()[childIndex + 1];

        int leftKeyNum = leftChild == null ? -1 : leftChild.getKeyNum();
        int rightKeyNum = rightChild == null ? -1 : rightChild.getKeyNum();

        return leftKeyNum > rightKeyNum ? leftChild : rightChild;
    }

    /**
     * 判断兄弟节点是否是左兄弟
     * 调用前必须保证类成员变量childIndex是赋过值的
     * @param siblingNode
     * @return
     */
    private boolean isLeftSibling(Node<K,V> siblingNode){
        return siblingNode.getParent().getChildren()[childIndex + 1] != siblingNode;
    }

    /**
     * 根据key查找对应节点(重载)
     * @param key 要查找的key
     * @return
     */
    private Node<K, V> searchNodeByKey(K key){
        return searchNodeByKey0(root, key);
    }

    private Node<K, V> searchNodeByKey0(Node<K,V> node,K key) {
        //先检查当前节点有没有key
        int index = binarySearchIndexOrSection(node.getKeys(), 0, node.getKeyNum() - 1, key);
        //当前节点的keys数组不存在key,那么index就是key落入keys的区间位置
        if (!isFind) {
            if (!node.isLeaf()) {
                //如果当前节点不是叶子节点,就去找key对应keys区间的那个孩子节点
                return searchNodeByKey0(node.getChildren()[index], key);
            }else {
                //当前叶子节点还是没有找到key,说明整棵树都没有这个key,直接返回null
                return null;
            }
        }else {
            //当前节点的keys中找到了key,返回当前节点,顺便把key所在keys的索引记录一下,这样全程就只需要一次二分查找
            keyIndex = index;
            return node;
        }

    }

    /**
     * 二分查找数组中的key
     * @param arr
     * @param left
     * @param right
     * @param key
     * @return 如果找到,就将全局变量isFind改为true并且返回key所在的索引;如果没找到,将全局变量isFind设置为false,并且返回key落入哪个区间section的孩子索引
     * @param <T>
     */
    private <T extends Comparable<T>> int binarySearchIndexOrSection(T[] arr, int left, int right, T key) {
        int mid = (left + right) / 2;
        int cmp = key.compareTo(arr[mid]);

        if (cmp == 0) {
            //当前节点找到key,将isFind置为true,并返回key所在的索引
            isFind = true;
            return mid;
        } else if (left >= right) {
            //最后一次查找还是找不到key,返回该key落入哪个区间section
            isFind = false;
            if (cmp < 0) {
                //比当前mid要小,说明key落入mid的左边,childIndex应该是mid
                return mid;
            }else {
                //比当前mid要小,说明key落入mid的右边,childIndex应该是mid+1
                return mid + 1;
            }
        } else if (cmp < 0) {
            return binarySearchIndexOrSection(arr, left, mid - 1, key);
        }else {
            return binarySearchIndexOrSection(arr, mid + 1, right, key);
        }
    }

    /**
     * 根据key获取对应value值
     * @param key
     * @return
     */
    public V get(K key) {
        //获取key所在的对应节点,此时keyIndex已经记录了key所在keys的索引位置
        Node<K, V> node = searchNodeByKey(key);
        if (node == null) {
            System.out.println("没有找到key,获取value失败!");
            return null;
        }
        return node.getValues()[keyIndex];
    }

    /**
     * 打印B树
     */
    public void printTree(){
        if (root.getKeyNum() == 0) {
            System.out.println("当前BTree为空树!");
            return;
        }
        printTree0(root);
        count = 0;
    }

    private void printTree0(Node<K,V> node){
        System.out.print("第" + count + "个节点:  ");
        count++;
        for (int i = 0; i < node.getKeyNum(); i++) {
            System.out.print(node.getKeys()[i] + "  ");
        }
        System.out.println();
        //当前节点不是叶子节点,找子节点
        if (!node.isLeaf()){
            for (int i = 0; i < node.getChildNum(); i++) {
                printTree0(node.getChildren()[i]);
            }
        }
    }

}

4、测试图例

4.1、测试 添加key

插入1到10

4.2、测试 删除key

4.2、测试 根据key查看对应的value

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值