数据结构与算法【B树】的Java实现+图解

目录

B树

特性

实现

节点准备

大体框架

实现分裂

实现新增

实现删除

完整代码


B树

也是一种自平衡的树形数据结构,主要用于管理磁盘上的数据管理(减少磁盘IO次数)。而之前说的AVL树与红黑树适合用于内存数据管理。存储一个100w的数据使用AVL存储,树高大约为20层(log_2100W),如果使用磁盘IO查询20次效率较低。

特性

度degree:指树中节点孩子数

阶order:指所有节点孩子数中最大值

一棵 B-树具有以下性质

特性1:每个节点 x 具有

  • 属性 n,表示节点 x 中 key 的个数
  • 属性 leaf,表示节点是否是叶子节点
  • 节点 key 可以有多个,以升序存储

特性2:每个节点最多具有m个孩子,其中m叫做B-树的阶

特性3:除根结点与叶子节点外,每个节点至少有ceil(m/2)个孩子,根节点不是叶子节点时,最少有两个孩子。叶子节点没有孩子

特性2:每个非叶子节点中的孩子数是 n + 1。而n的取值为ceil(m/2)-1<=n<=m-1。

特性3:最小度数t(节点的孩子数称为度)和节点中键数量的关系如下:

最小度数t键数量范围
21 ~ 3
32 ~ 5
43 ~ 7
......
n(n-1) ~ (2n-1)

其中,当节点中键数量达到其最大值时,即 3、5、7 ... 2n-1,需要分裂

特性4:叶子节点的深度都相同

实现

节点准备

B树的节点属性,与其他树不太相同,首先是key可以有多个,因此要设置为数组,孩子节点也未知,因此也要设置为数组。本应该还存在一个value属性,这里简化掉,不添加该属性。

static class Node {
    boolean leaf = true; //是否是叶子节点
    int keyNumber; //有效key
    int t; //最小度
    int[] keys; // keys数组
    Node[] children; //孩子节点数组

    public Node(int t) {
        this.t = t;
        this.keys = new int[2 * t - 1];
        this.children = new Node[2 * t];//最大孩子节点个数为为2*t
    }

    @Override
    public String toString() {
        return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));
    }

    /**
     * 根据key获取对应节点
     *
     * @param key
     * @return
     */
    Node get(int key) {
        int i = 0;
        while (i < keyNumber) {
            //如果在该节点找到,那么直接返回即可
            if (keys[i] == key) {
                return this;
            }
            //说明要找的元素可能在children[i]中
            if (keys[i] > key) {
                break;
            }
            i++;
        }
        //如果是叶子节点,直接返回null
        if (leaf) {
            return null;
        }
        return children[i].get(key);
    }

    /**
     * 指定位置插入元素
     *
     * @param key
     * @param index
     */
    void insertKey(int key, int index) {
        System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
        keys[index] = key;
        keyNumber++;
    }

    /**
     * 向节点中插入孩子节点
     * @param child
     * @param index
     */
    void insertChild(Node child, int index) {
        System.arraycopy(children, index, children, index + 1, keyNumber - index);
        children[index] = child;
    }
}

这里我采用了静态数组,因此需要多添加一个keyNumber参数来获取有效key的数量,如果使用ArrayList,可以通过size方法获取,因此不需要添加这个属性。

大体框架

public class BTree {
    private Node root;
    private int t;//最小度数
    final int MAX_KEY_NUMBER;//最大key数量
    final int MIN_KEY_NUMBER;//最小key数量。用于分裂使用

    public BTree(int t) {
        this.t = t;
        root = new Node(t);
        MAX_KEY_NUMBER = 2 * t - 1;
        MIN_KEY_NUMBER = 2 * t;
    }

    static class Node {
        boolean leaf = true; //是否是叶子节点
        int keyNumber; //有效key
        int t; //最小度
        int[] keys; // keys数组
        Node[] children; //孩子节点数组

        public Node(int t) {
            this.t = t;
            this.keys = new int[2 * t - 1];
            this.children = new Node[2 * t];//最大孩子节点个数为为2*t
        }

        @Override
        public String toString() {
            return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));
        }

        /**
         * 根据key获取对应节点
         *
         * @param key
         * @return
         */
        Node get(int key) {
            int i = 0;
            while (i < keyNumber) {
                //如果在该节点找到,那么直接返回即可
                if (keys[i] == key) {
                    return this;
                }
                //说明要找的元素可能在children[i]中
                if (keys[i] > key) {
                    break;
                }
                i++;
            }
            //如果是叶子节点,直接返回null
            if (leaf) {
                return null;
            }
            return children[i].get(key);
        }

        /**
         * 指定位置插入元素
         *
         * @param key
         * @param index
         */
        void insertKey(int key, int index) {
            System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
            keys[index] = key;
            keyNumber++;
        }

        /**
         * 向节点中插入孩子节点
         *
         * @param child
         * @param index
         */
        void insertChild(Node child, int index) {
            System.arraycopy(children, index, children, index + 1, keyNumber - index);
            children[index] = child;
        }
    }
}

实现分裂

先说分裂规律,当新增一个节点后使节点中的key满足2t-1,那么该节点就会被分裂。

  • 创建一个新的节点,暂时称为right节点(分裂后会在被分裂节点的右边)
  • 被分裂节点把 t 以后的 key 和 child 都拷贝到right节点
  • t-1 处的 key 插入到 parent 的 index 处,index 指被分裂节点作为孩子时的索引
  • right 节点作为 parent 的孩子插入到 index + 1 处

图示如下:

红色部分的意思是当前节点是父结点中作为孩子的下标。黑色部分是key,小数字代表key的下标。

起始存在一个t为3的B树。那么最大key就为 2*3-1 。此时作为父结点孩子下标为1的节点以及存在 4 个key,再添加一个key就会触发分裂。

现在,再添加一个新的key值 8 ,此时到达最大key数,触发分裂

此时分裂结束,分裂后结果如下

具体实现代码如下

private void split(Node parent, int index, Node split) {
    if (parent == null) {
        //说明分割根节点,除了需要创建right节点之外,还需要创建parent节点
        parent = new Node(t);
        parent.leaf = false;
        parent.insertChild(split, 0);
        root = parent;
    }
    Node right = new Node(t);
    //将被分裂节点的一部分key放入right节点中。
    System.arraycopy(split.keys, index, right.keys, 0, t - 1);
    //新建的节点与被分裂节点在同一层,因此leaf属性应该和被分裂节点一样
    right.leaf = split.leaf;
    right.keyNumber = t - 1;
    //如果被分裂节点不是叶子节点,也需要将孩子节点一并拷贝到right节点中
    if (!split.leaf) {
        System.arraycopy(split.children, t, right.children, 0, t);
    }
    split.keyNumber = t - 1;
    //将t-1节点放入父结点中
    int mid = split.keys[t - 1];
    parent.insertKey(mid, index);
    parent.insertChild(right, index + 1);
}

实现新增

  • 首先查找本节点中的插入位置 i,如果没有空位(key 被找到),应该走更新的逻辑。
  • 接下来分两种情况
    • 如果节点是叶子节点,可以直接插入了
    • 如果节点是非叶子节点,需要继续在 children[i] 处继续递归插入
  • 无论哪种情况,插入完成后都可能超过节点 keys 数目限制,此时应当执行节点分裂
    • 参数中的 parent 和 index 都是给分裂方法用的,代表当前节点父节点,和分裂节点是第几个孩子

具体实现代码如下

public void put(int key) {
    doPut(null, 0, root, key);
}

private void doPut(Node parent, int index, Node node, int key) {
    int i = 0;
    while (i < node.keyNumber && node.keys[i] < key) {
        i++;
    }
    // TODO i<node.keyNumber是否多余?
    if (i < node.keyNumber && node.keys[i] == key) {
        return;
    }
    if (node.leaf) {
        node.insertKey(key, i);
    } else {
        doPut(node, i, node.children[i], key);
    }
    if (isFull(node)) {
        split(parent, index, node);
    }
}

private boolean isFull(Node node) {
    return node.keyNumber == MAX_KEY_NUMBER;
}

实现删除

删除节点会存在下面几种情况

case 1:当前节点是叶子节点,没找到。直接返回null

case 2:当前节点是叶子节点,找到了。直接移除节点即可

case 3:当前节点是非叶子节点,没找到。递归寻找孩子节点是否存在该key

case 4:当前节点是非叶子节点,找到了。找到后驱节点交换key,并将交换后的key删除

case 5:删除后 key 数目 < 下限(不平衡)。需要进行调整

在兄弟节点中keyNumber数量充足的情况下可以通过旋转调整平衡。图示如下

现在要删除节点 2

删除之后,左侧孩子的key数量少于最小限度,因此需要进行一次左旋。

父结点 3 移动到左侧孩子节点中,右侧孩子节点中的第一个key 5 移动到父结点中,左旋结束。

但如果兄弟节点的key数量是最小限度,那么此时应该进行合并,而不是旋转。

合并时,我们通常选择将右侧的节点合并到左侧节点中去。图示如下

此时要删除key 3 ,右侧兄弟节点无法再向被删除节点提供key。

于是将右侧节点移除,同时将父结点的值与被移除节点的值都放在最初的左孩子节点中。

case 6:根节点

当经过合并之后,根结点可能会存在为null的情况,此时让根节点中的 0 号孩子替代掉根节点就好。

具体实现代码如下

/**
 * 移除指定元素
 *
 * @param key
 */
public void remove(int key) {
    doRemove(null,root,0, key);
}

private void doRemove(Node parent,Node node,int index, int key) {
    //首先要获取指定元素
    int i = 0;
    while (i < node.keyNumber && node.keys[i] < key) {
        i++;
    }

    if (node.leaf) {
        if (node.keys[i] == key) {
            //case 2:如果找到了并且是叶子节点
            node.removeKey(i);
        } else {
            //case 1:如果没找到并且是叶子节点
            return;
        }
    } else {
        if (node.keys[i] == key) {
            //case 4:如果找到了但不是叶子节点
            //找到后驱节点并交换位置
            Node child = node.children[i + 1];
            while (!child.leaf) {
                child = child.children[0];
            }
            int nextKey = child.keys[0];
            node.keys[i] = nextKey;
            //之所以不直接调用孩子节点的removeKey方法是为了避免删除后发生不平衡
            //child.removeKey(0);
            doRemove(node,child,i+1, nextKey);
        } else {
            //case 3:如果没找到但不是叶子节点
            doRemove(node,node.children[i],i, key);
        }
    }
    //如果删除后,节点中的key少于下限,那么需要进行调整
    if (node.keyNumber < MIN_KEY_NUMBER) {
        //平衡调整
        balance(parent,node,index);
    }
}

/**
 * 调整B树
 *
 * @param parent 父结点
 * @param node   被调整节点
 * @param index  被调整节点在父结点中的孩子数组下标
 */
private void balance(Node parent, Node node, int index) {
    //case 6 根节点
    if (node == root) {
        if (node.keyNumber==0 && node.children[0]!=null){
            root = node.children[0];
        }
        return;
    }
    Node leftChild = parent.leftSibling(index);
    Node rightChild = parent.rightSibling(index);
    //如果左边孩子节点中的key值充足
    if (leftChild != null && leftChild.keyNumber > MIN_KEY_NUMBER) {
        //将父结点中的key赋值给node
        node.insertKey(parent.keys[index - 1], 0);
        if (!leftChild.leaf) {
            //如果左侧孩子不是一个叶子节点,在旋转过后,会导致keysNumber+1!=children。
            //因此将多出来的孩子赋值更多出来一个key的被调整节点
            node.insertChild(leftChild.removeRightmostChild(), 0);
        }
        //将左孩子中最右侧元素赋值给父结点
        parent.keys[index - 1] = leftChild.removeRightmostKey();
        return;
    }
    //如果右边充足
    if (rightChild != null && rightChild.keyNumber > MIN_KEY_NUMBER) {
        node.insertKey(parent.keys[index], node.keyNumber);
        if (!rightChild.leaf) {
            node.insertChild(rightChild.removeLeftmostChild(), node.keyNumber);
        }
        parent.keys[index] = rightChild.removeLeftmostKey();
        return;
    }
    //合并
    //如果删除节点存在左兄弟,向左合并
    if (leftChild != null) {
        //将被删除节点从父结点上移除
        parent.removeChild(index);
        //将父结点的被移除节点的前驱节点移动到左兄弟上
        leftChild.insertKey(parent.removeKey(index - 1), leftChild.keyNumber);
        node.moveToTarget(leftChild);
    } else {
        //如果没有左兄弟,那么移除右兄弟节点,并将右兄弟移动到被删除节点上。
        parent.removeChild(index+1);
        node.insertKey(parent.removeKey(index),node.keyNumber);
        rightChild.moveToTarget(node);
    }
}

完整代码

public class BTree {
    private Node root;
    private int t;//最小度数
    private final int MAX_KEY_NUMBER;
    private final int MIN_KEY_NUMBER;

    public BTree(int t) {
        this.t = t;
        root = new Node(t);
        MAX_KEY_NUMBER = 2 * t - 1;
        MIN_KEY_NUMBER = t-1;
    }

    static class Node {
        boolean leaf = true; //是否是叶子节点
        int keyNumber; //有效key
        int t; //最小度
        int[] keys; // keys数组
        Node[] children; //孩子节点数组

        public Node(int t) {
            this.t = t;
            this.keys = new int[2 * t - 1];
            this.children = new Node[2 * t];//最大孩子节点个数为为2*t
        }

        @Override
        public String toString() {
            return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));
        }

        /**
         * 根据key获取对应节点
         *
         * @param key
         * @return
         */
        Node get(int key) {
            int i = 0;
            while (i < keyNumber) {
                //如果在该节点找到,那么直接返回即可
                if (keys[i] == key) {
                    return this;
                }
                //说明要找的元素可能在children[i]中
                if (keys[i] > key) {
                    break;
                }
                i++;
            }
            //如果是叶子节点,直接返回null
            if (leaf) {
                return null;
            }
            return children[i].get(key);
        }

        /**
         * 指定位置插入元素
         *
         * @param key
         * @param index
         */
        void insertKey(int key, int index) {
            System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
            keys[index] = key;
            keyNumber++;
        }

        /**
         * 向节点中插入孩子节点
         *
         * @param child
         * @param index
         */
        void insertChild(Node child, int index) {
            System.arraycopy(children, index, children, index + 1, keyNumber - index);
            children[index] = child;
        }

        //移除指定元素
        int removeKey(int index) {
            int t = keys[index];
            System.arraycopy(keys, index + 1, keys, index, --keyNumber - index);
            return t;
        }

        //移除最左边的元素
        int removeLeftmostKey() {
            return removeKey(0);
        }

        //移除最右边的元素
        int removeRightmostKey() {
            return removeKey(keyNumber - 1);
        }

        //移除指定位置的孩子节点
        Node removeChild(int index) {
            //获取被移除的节点
            Node t = children[index];
            //将被移除节点的后面元素向前移动一位
            System.arraycopy(children, index + 1, children, index, keyNumber - index);
            //将之前最后一位的引用释放
            children[keyNumber] = null;
            //返回被引用元素
            return t;
        }

        //移除最左边的孩子节点
        Node removeLeftmostChild() {
            return removeChild(0);
        }

        //移除最右边的孩子节点
        Node removeRightmostChild() {
            return removeChild(keyNumber);
        }

        //移动指定节点到目标节点
        void moveToTarget(Node target) {
            int start = target.keyNumber;
            if (!leaf) {
                for (int i = 0; i <= keyNumber; i++) {
                    target.children[start + i] = children[i];
                }
            }
            for (int i = 0; i < keyNumber; i++) {
                target.keys[target.keyNumber++] = keys[i];
            }
        }

        //获取指定孩子节点的左边节点
        Node leftSibling(int index) {
            return index > 0 ? children[index - 1] : null;
        }

        //获取指定孩子节点的右边节点
        Node rightSibling(int index) {
            return index == keyNumber ? null : children[index + 1];
        }

    }

    /**
     * 查询key值是否在树中
     *
     * @param key
     * @return
     */
    public boolean contains(int key) {
        return root.get(key) != null;
    }

    /**
     * 新增节点
     *
     * @param key
     */
    public void put(int key) {
        doPut(null, 0, root, key);
    }

    //index的作用是,给分割方法提供参数
    private void doPut(Node parent, int index, Node node, int key) {
        //寻找插入位置
        int i = 0;
        while (i < node.keyNumber && node.keys[i] < key) {
            i++;
        }
        //如果找到了,直接更新
        if (node.keys[i] == key) {
            return;
        }
        //如果没找到,查看是否是叶子节点,如果不是,向下寻找
        if (node.leaf) {
            node.insertKey(key, i);
        } else {
            doPut(node, i, node.children[i], key);
        }
        if (isFull(node)) {
            split(parent, index, node);
        }
    }

    private boolean isFull(Node node) {
        return node.keyNumber == MAX_KEY_NUMBER;
    }

    /**
     * 分裂节点
     *
     * @param parent
     * @param index 分割节点在父结点的孩子下标
     * @param split
     */
    private void split(Node parent, int index, Node split) {
        if (parent == null) {
            //说明分割根节点,除了需要创建right节点之外,还需要创建parent节点
            parent = new Node(t);
            parent.leaf = false;
            parent.insertChild(split, 0);
            root = parent;
        }
        Node right = new Node(t);
        //将被分裂节点的一部分key放入right节点中。
        System.arraycopy(split.keys, t, right.keys, 0, t - 1);
        //新建的节点与被分裂节点在同一层,因此leaf属性应该和被分裂节点一样
        right.leaf = split.leaf;
        right.keyNumber = t - 1;
        //如果被分裂节点不是叶子节点,也需要将孩子节点一并拷贝到right节点中
        if (!split.leaf) {
            System.arraycopy(split.children, t, right.children, 0, t);
            for (int i =t;i<=split.keyNumber;i++){
                split.children[i]=null;
            }
        }
        split.keyNumber = t - 1;
        //将t-1节点放入父结点中
        int mid = split.keys[t - 1];
        parent.insertKey(mid, index);
        parent.insertChild(right, index + 1);
    }

    /**
     * 移除指定元素
     *
     * @param key
     */
    public void remove(int key) {
        doRemove(null,root,0, key);
    }

    private void doRemove(Node parent,Node node,int index, int key) {
        //首先要获取指定元素
        int i = 0;
        while (i < node.keyNumber && node.keys[i] < key) {
            i++;
        }

        if (node.leaf) {
            if (node.keys[i] == key) {
                //case 2:如果找到了并且是叶子节点
                node.removeKey(i);
            } else {
                //case 1:如果没找到并且是叶子节点
                return;
            }
        } else {
            if (node.keys[i] == key) {
                //case 4:如果找到了但不是叶子节点
                //找到后驱节点并交换位置
                Node child = node.children[i + 1];
                while (!child.leaf) {
                    child = child.children[0];
                }
                int nextKey = child.keys[0];
                node.keys[i] = nextKey;
                //之所以不直接调用孩子节点的removeKey方法是为了避免删除后发生不平衡
                //child.removeKey(0);
                doRemove(node,child,i+1, nextKey);
            } else {
                //case 3:如果没找到但不是叶子节点
                doRemove(node,node.children[i],i, key);
            }
        }
        //如果删除后,节点中的key少于下限,那么需要进行调整
        if (node.keyNumber < MIN_KEY_NUMBER) {
            //平衡调整
            balance(parent,node,index);
        }
    }

    /**
     * 调整B树
     *
     * @param parent 父结点
     * @param node   被调整节点
     * @param index  被调整节点在父结点中的孩子数组下标
     */
    private void balance(Node parent, Node node, int index) {
        //case 6 根节点
        if (node == root) {
            if (node.keyNumber==0 && node.children[0]!=null){
                root = node.children[0];
            }
            return;
        }
        Node leftChild = parent.leftSibling(index);
        Node rightChild = parent.rightSibling(index);
        //如果左边孩子节点中的key值充足
        if (leftChild != null && leftChild.keyNumber > MIN_KEY_NUMBER) {
            //将父结点中的key赋值给node
            node.insertKey(parent.keys[index - 1], 0);
            if (!leftChild.leaf) {
                //如果左侧孩子不是一个叶子节点,在旋转过后,会导致keysNumber+1!=children。
                //因此将多出来的孩子赋值更多出来一个key的被调整节点
                node.insertChild(leftChild.removeRightmostChild(), 0);
            }
            //将左孩子中最右侧元素赋值给父结点
            parent.keys[index - 1] = leftChild.removeRightmostKey();
            return;
        }
        //如果右边充足
        if (rightChild != null && rightChild.keyNumber > MIN_KEY_NUMBER) {
            node.insertKey(parent.keys[index], node.keyNumber);
            if (!rightChild.leaf) {
                node.insertChild(rightChild.removeLeftmostChild(), node.keyNumber);
            }
            parent.keys[index] = rightChild.removeLeftmostKey();
            return;
        }
        //合并
        //如果删除节点存在左兄弟,向左合并
        if (leftChild != null) {
            //将被删除节点从父结点上移除
            parent.removeChild(index);
            //将父结点的被移除节点的前驱节点移动到左兄弟上
            leftChild.insertKey(parent.removeKey(index - 1), leftChild.keyNumber);
            node.moveToTarget(leftChild);
        } else {
            //如果没有左兄弟,那么移除右兄弟节点,并将右兄弟移动到被删除节点上。
            parent.removeChild(index+1);
            node.insertKey(parent.removeKey(index),node.keyNumber);
            rightChild.moveToTarget(node);
        }
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/182945.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

4.常见面试题--操作系统

特点&#xff1a;并发性、共享性、虚拟性、异步性。 Windows 和 Linux 内核差异 对于内核的架构⼀般有这三种类型&#xff1a; ● 宏内核&#xff0c;包含多个模块&#xff0c;整个内核像⼀个完整的程序&#xff1b; ● 微内核&#xff0c;有⼀个最⼩版本的内核&#xff0…

docker国内镜像加速

创建或修改 /etc/docker/daemon.json 文件&#xff0c;修改为如下形式 {"registry-mirrors": ["https://registry.docker-cn.com","http://hub-mirror.c.163.com","https://docker.mirrors.ustc.edu.cn"] } Docker中国区官方镜像htt…

【广州华锐互动】VR线上课件制作软件满足数字化教学需求

随着科技的不断发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术在教学领域的应用逐渐成为趋势。其中&#xff0c;广州华锐互动开发的VR线上课件制作软件更是备受关注。这种工具为教师提供了便捷的制作VR课件的手段&#xff0c;使得VR教学成为可能&#xff0c;极大地丰…

python cv2.imread()和Image.open()的区别和联系

文章目录 1. cv2.imread()1.1 cv2.imread参数说明1.2 注意事项 2. Image.open()3. cv2.imread()与Image.open()相互转化3.1 cv2.imread()转成Image.open()&#xff1a;Image.fromarray()3.2 Image.open()转成cv2.imread()&#xff1a;np.array() 1. cv2.imread() cv2.imread()…

MyBatisPlus总结

MyBatis-Plus时Mybatis的Best Partner MyBatis-Plus (opens new window)&#xff08;简称 MP&#xff09;是一个 MyBatis (opens new window)的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 特性 无侵入损耗小强大的 CR…

Linux(Centos)上使用crontab实现定时任务(定时执行脚本)

场景 Windows中通过bat定时执行命令和mysqldump实现数据库备份&#xff1a; Windows中通过bat定时执行命令和mysqldump实现数据库备份_mysqldump bat-CSDN博客 上面讲windows中使用bat实现定时任务的方式&#xff0c;如果是在linux上可以通过crontab实现。 cron是服务名称。…

Navicat 技术指引 | 适用于 GaussDB 的查询编辑器

Navicat Premium&#xff08;16.2.8 Windows版或以上&#xff09; 已支持对 GaussDB 主备版的管理和开发功能。它不仅具备轻松、便捷的可视化数据查看和编辑功能&#xff0c;还提供强大的高阶功能&#xff08;如模型、结构同步、协同合作、数据迁移等&#xff09;&#xff0c;这…

【开源】基于Vue.js的海南旅游景点推荐系统的设计和实现

项目编号&#xff1a; S 023 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S023&#xff0c;文末获取源码。} 项目编号&#xff1a;S023&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用户端2.2 管理员端 三、系统展示四…

海报设计必备:揭秘5款炙手可热的设计工具

1.即时设计&#xff1a;能实现在线协作的海报设计软件 即时设计作为 2020 年上线的国产设计工具&#xff0c;目前已经有了超百万的注册用户&#xff0c;获得了广大设计师的一致好评。与其他传统海报设计软件相比&#xff0c;即时设计具有这几个优点&#xff1a;一是所有功能都…

【DevOps】Git 图文详解(九):工作中的 Git 实践

本系列包含&#xff1a; Git 图文详解&#xff08;一&#xff09;&#xff1a;简介及基础概念Git 图文详解&#xff08;二&#xff09;&#xff1a;Git 安装及配置Git 图文详解&#xff08;三&#xff09;&#xff1a;常用的 Git GUIGit 图文详解&#xff08;四&#xff09;&a…

【算法】缓存淘汰算法

目录 1.概述2.代码实现2.1.FIFO2.2.LRU2.3.LFU2.4.Clock2.5.Random 3.应用 1.概述 缓存淘汰策略是指在缓存容量有限的情况下&#xff0c;当缓存空间不足时决定哪些缓存项应当被移除的策略。缓存淘汰策略的目标是尽可能地保持缓存命中率高&#xff0c;同时合理地利用有限的缓存…

最小二乘线性回归

​ 线性回归&#xff08;linear regression&#xff09;&#xff1a;试图学得一个线性模型以尽可能准确地预测实际值的输出。 以一个例子来说明线性回归&#xff0c;假设银行贷款会根据 年龄 和 工资 来评估可放款的额度。即&#xff1a; ​ 数据&#xff1a;工资和年龄&…

CSS特效017:球体涨水的效果

CSS常用示例100专栏目录 本专栏记录的是经常使用的CSS示例与技巧&#xff0c;主要包含CSS布局&#xff0c;CSS特效&#xff0c;CSS花边信息三部分内容。其中CSS布局主要是列出一些常用的CSS布局信息点&#xff0c;CSS特效主要是一些动画示例&#xff0c;CSS花边是描述了一些CSS…

app小程序定制的重点|软件定制开发|网站搭建

app小程序定制的重点|软件定制开发|网站搭建 App小程序定制开发是近年来快速发展的一项技术服务&#xff0c;随着移动互联网的普及和用户需求的不断升级&#xff0c;越来越多的企业和个人开始关注和需求定制化的小程序开发。那么&#xff0c;对于app小程序定制开发来说&#xf…

React中如何解决点击<Tree>节点前面三角区域不触发onClick事件

React中如何解决点击节点前面三角区域不触发onClick事件&#xff0c;如何区别‘左边’和‘右边’区域点击逻辑呢&#xff1f;&#xff08;Tree引用开源组件TDesign&#xff09; 只需要在onClick里面加限制一下就行&#xff1a; <TreeexpandMutexactivabletransitiondata{t…

使用XHProf查找PHP性能瓶颈

使用XHProf查找PHP性能瓶颈 XHProf是facebook 开发的一个测试php性能的扩展&#xff0c;本文记录了在PHP应用中使用XHProf对PHP进行性能优化&#xff0c;查找性能瓶颈的方法。 下载 网上很多是编译安装xhprof-0.9.4版本&#xff0c;应该是用php5&#xff0c;在php8.0下编译x…

C++语法知识点-vector+子数组

C语法知识点-vector子数组 一维数组定义无参数有参数迭代器扩容操作reserve 二维数组 vector 定义创建m*n的二维vectorvector< vector<int> > v(m, vector<int>(n) ) 初始化定义vector常用函数的实例分析访问操作resize 函数push _back ( )pop_back()函数siz…

【数据结构/C++】线性表_顺序表的基本操作

#include <iostream> using namespace std; #define MaxSize 10 // 1. 顺序表 // 静态分配 typedef struct {int data[MaxSize];int length; // 当前长度 } SqList; // 静态分配初始化顺序表 void InitList(SqList &L) {for (int i 0; i < MaxSize; i){L.data[i]…

基于yolov2深度学习网络的喝水行为检测系统matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1、YOLOv2网络原理 4.2、基于YOLOv2的喝水行为检测 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 clc; clear; close all; warning off;…

Django之中间件与CSRF_TOKEN

文章目录 一、什么是中间件二、中间件有什么用三、Django自定义中间件中间件中主要方法及作用创建自定义中间件的步骤&#xff1a;process_request与process_response方法process_view方法process_exceptionprocess_template_response&#xff08;不常用&#xff09; 四、CSRF_…