【算法笔记】有序表——相关题目

目录

《【算法笔记】有序表------AVL树》
《【算法笔记】有序表------SB树》
《【算法笔记】有序表------跳表》
《【算法笔记】有序表------相关题目》


1、有序表的改造思路

  • 有序表,有序表是指在插入元素时,会根据元素的大小,自动将元素插入到合适的位置,从而保证了表中元素的有序性的结构。
  • 面试场景中主要用到的有序表是:AVL树、红黑树、跳表、SB树等。
  • 但是基础的这些结构只是说明了其实如何保持平衡和查找元素的,如果遇到特殊的需求,就要改写有序表,从而让其适配新的需求。
  • 有序表的改造思路主要有以下几个方面:

1.1、如何处理重复元素的问题

  • 正常的有序表中是不容许插入重复元素的,但是很多的题目中是有重复元素的,如果要用有序表的方式来做题,就要改造有序表,让其能够支持重复元素。
  • 支持重复元素有两个方法,其一是给有序表的节点加入一个字段,用来记录当前节点的个数;其二是在SB树中,复用size字段,用一个封装结构来代替原来的key。
  • 给节点加入一个字段的方式很容易理解,这里我们单独讲一个SB树中复用size字段的方式。
  • 在SB树中,原来的size是指的以当前节点为头的子树的节点个数,并不支持重复字段,这个时候我们可以在调用SBT的时候,将key做一个封装,让其支持重复元素,
  • 比如我们可以将数组中的元素封装成一个类,类中包含数组的下标index和值value,这样就可以先用value来排序,
  • 在value相等的情况下用index排序,这样就可以支持重复元素了。

1.2、如何获取第i个元素,即用数组的方式操作有序表

  • 正常的有序表是通过元素的大小比较来确定元素的位置的,但是如果要按照插入顺序的方式来操作有序表,我们就可以在SB树上进行改写,
  • 具体的思路就是让原来节点大小的size字段,用来记录插入的顺序,这样的话,按照中序遍历的方式,节点的遍历顺序就代表了index。
  • 对于某一个节点a,其左子树上都是比a插入早的元素,右子树上都是比a插入晚的元素,因为原来的size代表了节点的大小(包含重复元素),
  • 这样右侧元素的下标就是当前节点的size + 右侧节点的size,而且左旋和右旋操作不影响这个顺序。
  • 在插入元素的时候,对比需要操作的index,如果小于当前节点的size,就递归插入到左子树,否则递归插入到右子树。

1.3、如何获取小于某个值的元素个数

  • 这个可以直接服用SB树的size,因为SBT是直接支持用index来获取第index个元素的,只是原始的不支持重复元素,
  • 如果要支持重复元素,我们可以直接将添加到SBT中的元素做一个封装,比如数组中的元素,我们可以将下标index和值value做一个封装,封装类先用value排序,
  • 在value相等的情况下用index排序,这样就利用SBT的结构天然支持重复元素和获取指定下标的元素了。,
  • 对于需要查找小于某个树的节点个数,如果当前数小于当前节点的size,直接在左树上找即可,
  • 如果当前数大于某个节点的size,则将当前的size累加,然后在右子树上找小于当前数-size的节点个数即可

2、题目一:区间和的个数

  • 题目一:区间和的个数
  • 给定一个数组arr,和两个整数a和b(a<=b)。求arr中有多少个子数组,累加和在[a,b]这个范围上。返回达标的子数组数量
  • 测试链接:https://leetcode.cn/problems/count-of-range-sum
  • 题目解析:
    • 这种子数组的题目,有两种求解思路,算出来的结果是一样的:
    • 1、从某个下标i开始,到结束统计有多少个子数组,
    • 2、以某个下标i结束,从0开始统计有多少个子数组
    • 两种思路只是考虑问题的方向不同,但是结果是一样的。

2.1、暴力方法加累加和数组的求法

  • 暴力方法加累加和数组的求法
  • 思路:
    • 像这种求累加和的方式,可以将原来的数组做成一个累加和,然后根据公式求解:

    • 累加和算法:

    • 1、sum[j]表示从0到j的累加和

    • 2、sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0...j] - sum[0...i-1]

    • 提交时方法名改为:countRangeSum --> 会超时

java 复制代码
    /**
     * 暴力方法加累加和数组的求法
     * 思路:
     * 像这种求累加和的方式,可以将原来的数组做成一个累加和,然后根据公式求解:
     * 累加和算法:
     * 1、sum[j]表示从0到j的累加和
     * 2、sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1]
     * <br>
     * 提交时方法名改为:countRangeSum --> 会超时
     */
    public static int countRangeSumWithSumArr(int[] arr, int lower, int upper) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int res = 0;
        // 构建前缀和数组,为了防止溢出,要用long
        long[] sumArr = new long[arr.length];
        sumArr[0] = arr[0];
        for (int i = 1; i < arr.length; i++) {
            sumArr[i] = sumArr[i - 1] + arr[i];
        }
        for (int i = 0; i < arr.length; i++) {
            for (int j = i; j < arr.length; j++) {
                long sum = 0;
                if (i == 0) {
                    sum = sumArr[j];
                } else {
                    sum = sumArr[j] - sumArr[i - 1];
                }
                if (sum >= lower && sum <= upper) {
                    res++;
                }
            }
        }
        return res;
    }

2.2、改写SB树的求法

  • 改写SB树的求法:

  • 思路:

    • 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0...j] - sum[0...i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
    • 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
    • 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0...j] - sum[0...i-1]也在[lower,upper]范围,
    • 4.所以如果sum[0...j]为x,为了能让sum[0...j]满足[lower,upper]范围,那么sum[0...i-1]必须在[x-upper,x-lower]范围
    • 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
    • 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
  • 利用SB树解决j前面有多少个累加和在[x-upper,x-lower]上的思路:

    • 同样是利用累加和数组的方法,将累加和的数组依次加入到SB树中,然后求出小于等于某个值的个数,利用累加和数组的计算公式,就可以求出整体的差值,然后判断区间。
    • 因为累加和数组不需要删除,所以只需要提供add和查询小于某个值的个数的方法即可。
    • 同时,因为这里需要考虑重复值,我们可以用新增一个字段的方式,来记录真个树的值,方便查询。
    • 这里用新增一个字段allCount的方式,和原来的size字段区分开,这样方便理解改造的过程。
    • 某个节点的allCount就表示以该节点为根的树,所有节点的累加和的个数。
    • 这样,这个节点的值的个数就是allCount - (左子树的allCount) - (右子树的allCount)
  • 过程:

    • 1、新建一个改造的SB树结构,用来插入和查询小于某个值的元素

    • 2、用一个变量sum来记录累计到当前位置的累加和,到了某个位置i,此时的问题就变成了必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上

    • 3、从0开始遍历数组,每到一个位置,累加和sun累计当前的值arr[i],然后计算出新的需要判断的边界sum-upper和sum-lower

    • 4、用SB树查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上

    • 因为要求的是[sum-upper,sum-lower]这个闭区间的范围,这个范围要想包含sum-upper的边界,就要减去小于sum-upper的值,即不能包含sum-upper,否则就会少算相等的一部分值。

    • 因为SB树查询的就是小于某个树的个数,所以问题就转成了从SB中查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上

    • 提交时方法名改为:countRangeSum

java 复制代码
    /**
     * 改写SB树的求法:
     * 思路:
     * 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
     * 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
     * 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0..j] - sum[0..i-1]也在[lower,upper]范围,
     * 4.所以如果sum[0..j]为x,为了能让sum[0..j]满足[lower,upper]范围,那么sum[0..i-1]必须在[x-upper,x-lower]范围
     * 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
     * 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
     * <br>
     * 利用SB树解决j前面有多少个累加和在[x-upper,x-lower]上的思路:
     * 同样是利用累加和数组的方法,将累加和的数组依次加入到SB树中,然后求出小于等于某个值的个数,利用累加和数组的计算公式,就可以求出整体的差值,然后判断区间。
     * 因为累加和数组不需要删除,所以只需要提供add和查询小于某个值的个数的方法即可。
     * 同时,因为这里需要考虑重复值,我们可以用新增一个字段的方式,来记录真个树的值,方便查询。
     * 这里用新增一个字段allCount的方式,和原来的size字段区分开,这样方便理解改造的过程。
     * 某个节点的allCount就表示以该节点为根的树,所有节点的累加和的个数。
     * 这样,这个节点的值的个数就是allCount - (左子树的allCount) - (右子树的allCount)
     * <br>
     * 过程:
     * 1、新建一个改造的SB树结构,用来插入和查询小于某个值的元素
     * 2、用一个变量sum来记录累计到当前位置的累加和,到了某个位置i,此时的问题就变成了必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
     * 3、从0开始遍历数组,每到一个位置,累加和sun累计当前的值arr[i],然后计算出新的需要判断的边界sum-upper和sum-lower
     * 4、用SB树查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
     * 因为要求的是[sum-upper,sum-lower]这个闭区间的范围,这个范围要想包含sum-upper的边界,就要减去小于sum-upper的值,即不能包含sum-upper,否则就会少算相等的一部分值。
     * 因为SB树查询的就是小于某个树的个数,所以问题就转成了从SB中查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
     * <br>
     * 提交时方法名改为:countRangeSum
     */
    public static int countRangeSumWithSizeBalanced(int[] arr, int lower, int upper) {
        if (arr == null || arr.length == 0 || lower > upper) {
            return 0;
        }
        // SB的改造结构
        SizeBalancedTreeSet sbt = new SizeBalancedTreeSet();
        // 累计和的变量
        long sum = 0;
        // 记录最终的结果
        int ans = 0;
        // 一个数都没有的时候,就已经有一个前缀和累加和为0
        sbt.add(0);
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
            // 计算出新的需要判断的边界sum-upper和sum-lower
            long lowerBound = sum - upper;
            long upperBound = sum - lower + 1;
            // 用SB树查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
            ans += sbt.lessKeySize(upperBound) - sbt.lessKeySize(lowerBound);
            // 将当前的累加和加入到SB树中
            sbt.add(sum);
        }
        return ans;
    }

    /**
     * 改写的SB树,新增一个字段allCount,记录以该节点为根的树,所有节点的累加和的个数。
     */
    private static class SizeBalancedTreeSet {
        // root节点
        private Node root;
        // 用一个set来快速判断某个值有没有添加过
        private Set<Long> set = new HashSet<>();


        /**
         * 右旋
         */
        private Node rightRotate(Node cur) {
            if (cur == null || cur.left == null) {
                return cur;
            }
            // 先记录当前节点的相同的个数
            int sameCount = cur.allCount - cur.left.allCount - (cur.right != null ? cur.right.allCount : 0);
            Node leftNode = cur.left;
            cur.left = leftNode.right;
            leftNode.right = cur;
            // 更新size,旋转对总的size不变,所以此时根节点的size就是原来cur的size
            leftNode.size = cur.size;
            cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
            // 更新allCount,旋转对节点数量不变,所以此时的根节点leftNode的allCount是原来的cur的allCount
            leftNode.allCount = cur.allCount;
            cur.allCount = (cur.left != null ? cur.left.allCount : 0) + sameCount + (cur.right != null ? cur.right.allCount : 0);
            return leftNode;
        }

        /**
         * 左旋
         */
        private Node leftRotate(Node cur) {
            if (cur == null || cur.right == null) {
                return cur;
            }
            int sameCount = cur.allCount - cur.right.allCount - (cur.left != null ? cur.left.allCount : 0);
            Node rightNode = cur.right;
            cur.right = rightNode.left;
            rightNode.left = cur;
            // 更新size
            rightNode.size = cur.size;
            cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
            // 更新allCount
            rightNode.allCount = cur.allCount;
            cur.allCount = sameCount + (cur.right != null ? cur.right.allCount : 0) + (cur.left != null ? cur.left.allCount : 0);
            return rightNode;
        }

        /**
         * 平衡调整
         */
        private Node maintain(Node cur) {
            if (cur == null) {
                return null;
            }
            int leftSize = cur.left != null ? cur.left.size : 0;
            int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
            int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
            int rightSize = cur.right != null ? cur.right.size : 0;
            int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
            int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
            if (leftLeftSize > rightSize) {
                // LL
                cur = rightRotate(cur);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (leftRightSize > rightSize) {
                // LR
                cur.left = leftRotate(cur.left);
                cur = rightRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (rightRightSize > leftSize) {
                // RR
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur = maintain(cur);
            } else if (rightLeftSize > leftSize) {
                // RL
                cur.right = rightRotate(cur.right);
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            }
            return cur;
        }


        /**
         * 递归添加元素
         */
        private Node add(Node cur, long key, boolean contains) {
            if (cur == null) {
                return new Node(key);
            }
            cur.allCount++;
            // key相同,直接返回
            if (key == cur.key) {
                return cur;
            }
            // 递归添加,先判断是否已经存在,如果已经存在,添加的时候只需要更新allCount字段,不需要更新size字段
            if (!contains) {
                cur.size++;
            }
            if (key < cur.key) {
                cur.left = add(cur.left, key, contains);
            } else {
                cur.right = add(cur.right, key, contains);
            }
            return maintain(cur);
        }

        /**
         * 添加元素
         */
        public void add(long key) {
            boolean contains = set.contains(key);
            // 调用递归add方法添加
            this.root = add(this.root, key, contains);
            if (!contains) {
                this.set.add(key);
            }
        }

        /**
         * 获得小于某个key的个数
         */
        public int lessKeySize(long key) {
            Node cur = root;
            int ans = 0;
            while (cur != null) {
                if (key == cur.key) {
                    // 相等,累计上左侧的值即可
                    return ans + (cur.left != null ? cur.left.allCount : 0);
                } else if (key < cur.key) {
                    // 小于的话,从左侧查找
                    cur = cur.left;
                } else {
                    // 累计上左侧和当前的值,往右查找,
                    // 累计左侧和当前的重复值,就是用当前的减去右侧的值
                    ans += cur.allCount - (cur.right != null ? cur.right.allCount : 0);
                    cur = cur.right;
                }
            }
            return ans;
        }

        class Node {
            private long key;
            private Node left;
            private Node right;
            private int size;
            // 以该节点为根的树,所有节点的累加和的个数
            private int allCount;

            public Node(long key) {
                this.key = key;
                this.size = 1;
                this.allCount = 1;
            }
        }
    }

2.3、改写归并排序的方法

  • 改写归并排序的方法:

  • 思路:

    • 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0...j] - sum[0...i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
    • 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
    • 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0...j] - sum[0...i-1]也在[lower,upper]范围,
    • 4.所以如果sum[0...j]为x,为了能让sum[0...j]满足[lower,upper]范围,那么sum[0...i-1]必须在[x-upper,x-lower]范围
    • 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
    • 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
      1. 用归并排序方法求解,对sum数组进行排序,在分解前,就可以求出arr[0...i]是否满足,即sum[i]是不是在[lower,upper]范围,
      1. 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
      1. 最后返回所有的个数和,就是求解的个数
  • 过程:
    *

    1. 先求前缀和数组sum,sum[i]表示arr[0...i]的累加和
      1. 递归处理sum数组,求在sum[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围,结束递归的条件L==R,判断sum[L]是否达标,代表了arr[0...L]这个数组上的累加和是不是达标
      1. 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
      1. 最后返回所有的个数和,就是求解的个数
    • 提交时方法名改为:countRangeSum

java 复制代码
    /**
     * 改写归并排序的方法:
     * 思路:
     * 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
     * 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
     * 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0..j] - sum[0..i-1]也在[lower,upper]范围,
     * 4.所以如果sum[0..j]为x,为了能让sum[0..j]满足[lower,upper]范围,那么sum[0..i-1]必须在[x-upper,x-lower]范围
     * 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
     * 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
     * 7. 用归并排序方法求解,对sum数组进行排序,在分解前,就可以求出arr[0...i]是否满足,即sum[i]是不是在[lower,upper]范围,
     * 8. 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
     * 9. 最后返回所有的个数和,就是求解的个数
     * <br>
     * 过程:
     * 1. 先求前缀和数组sum,sum[i]表示arr[0...i]的累加和
     * 2. 递归处理sum数组,求在sum[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围,结束递归的条件L==R,判断sum[L]是否达标,代表了arr[0...L]这个数组上的累加和是不是达标
     * 3. 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
     * 4. 最后返回所有的个数和,就是求解的个数
     * <br>
     * 提交时方法名改为:countRangeSum
     */
    public static int countRangeSumWithMergeSort(int[] arr, int lower, int upper) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        // 构建前缀和数组,为了防止溢出,要用long类型
        long[] sum = new long[arr.length];
        sum[0] = arr[0];
        for (int i = 1; i < arr.length; i++) {
            sum[i] = sum[i - 1] + arr[i];
        }
        // 递归处理sum数组,求在sum[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围
        return process(sum, 0, sum.length - 1, lower, upper);
    }

    /**
     * 递归处理函数:
     * 求在arr[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围
     * 因为已经求了累加和sum数组,所以直接用传入sum就可以了,不需要再传arr数组
     */
    private static int process(long[] sum, int L, int R, int lower, int upper) {
        if (L == R) {
            // 代表arr[0...L]这个数组上的累加和是不是达标
            return sum[L] >= lower && sum[L] <= upper ? 1 : 0;
        }
        // 递归处理左右两边
        int M = L + ((R - L) >> 1);
        return process(sum, L, M, lower, upper) + process(sum, M + 1, R, lower, upper)
                + merge(sum, L, M, R, lower, upper);
    }

    /**
     * 合并函数:
     * 求在arr[L...M]和arr[M+1...R]合并的过程中,求出多少个累加和在[lower,upper]范围
     * 因为已经求了累加和sum数组,所以直接用传入sum就可以了,不需要再传arr数组
     * 在合并前,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
     */
    public static int merge(long[] sum, int L, int M, int R, int lower, int upper) {
        int ans = 0;
        int windowL = L;
        int windowR = L;
        /**
         * 先求出右组中每个数x,左组中有多少个数,位于[x-upper,x-lower]范围
         * [windowL, windowR)
         * 因为左组和右组都是有序的,可以用滑动窗口的方法,求左组中有多少个数,位于[x-upper,x-lower]范围
         */
        for (int i = M + 1; i <= R; i++) {
            long min = sum[i] - upper;
            long max = sum[i] - lower;
            // 先移动windowR,找到第一个大于max的位置
            while (windowR <= M && sum[windowR] <= max) {
                windowR++;
            }
            // 再移动windowL,找到第一个大于等于min的位置
            while (windowL <= M && sum[windowL] < min) {
                windowL++;
            }
            ans += windowR - windowL;
        }
        // 正常合并
        long[] help = new long[R - L + 1];
        int index = 0;
        int p1 = L;
        int p2 = M + 1;
        while (p1 <= M && p2 <= R) {
            help[index++] = sum[p1] <= sum[p2] ? sum[p1++] : sum[p2++];
        }
        while (p1 <= M) {
            help[index++] = sum[p1++];
        }
        while (p2 <= R) {
            help[index++] = sum[p2++];
        }
        for (int i = 0; i < help.length; i++) {
            sum[L + i] = help[i];
        }
        return ans;
    }

整体代码和测试如下:

java 复制代码
import java.util.HashSet;
import java.util.Set;

/**
 * 题目一:区间和的个数
 * 给定一个数组arr,和两个整数a和b(a<=b)。求arr中有多少个子数组,累加和在[a,b]这个范围上。返回达标的子数组数量
 * 测试链接:https://leetcode.cn/problems/count-of-range-sum
 * 题目解析:
 * 这种子数组的题目,有两种求解思路,算出来的结果是一样的:
 * 1、从某个下标i开始,到结束统计有多少个子数组,
 * 2、以某个下标i结束,从0开始统计有多少个子数组
 * 两种思路只是考虑问题的方向不同,但是结果是一样的。
 */
public class Q1_CountofRangeSum {
    /**
     * 暴力方法加累加和数组的求法
     * 思路:
     * 像这种求累加和的方式,可以将原来的数组做成一个累加和,然后根据公式求解:
     * 累加和算法:
     * 1、sum[j]表示从0到j的累加和
     * 2、sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1]
     * <br>
     * 提交时方法名改为:countRangeSum --> 会超时
     */
    public static int countRangeSumWithSumArr(int[] arr, int lower, int upper) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int res = 0;
        // 构建前缀和数组,为了防止溢出,要用long
        long[] sumArr = new long[arr.length];
        sumArr[0] = arr[0];
        for (int i = 1; i < arr.length; i++) {
            sumArr[i] = sumArr[i - 1] + arr[i];
        }
        for (int i = 0; i < arr.length; i++) {
            for (int j = i; j < arr.length; j++) {
                long sum = 0;
                if (i == 0) {
                    sum = sumArr[j];
                } else {
                    sum = sumArr[j] - sumArr[i - 1];
                }
                if (sum >= lower && sum <= upper) {
                    res++;
                }
            }
        }
        return res;
    }

    /**
     * 改写SB树的求法:
     * 思路:
     * 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
     * 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
     * 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0..j] - sum[0..i-1]也在[lower,upper]范围,
     * 4.所以如果sum[0..j]为x,为了能让sum[0..j]满足[lower,upper]范围,那么sum[0..i-1]必须在[x-upper,x-lower]范围
     * 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
     * 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
     * <br>
     * 利用SB树解决j前面有多少个累加和在[x-upper,x-lower]上的思路:
     * 同样是利用累加和数组的方法,将累加和的数组依次加入到SB树中,然后求出小于等于某个值的个数,利用累加和数组的计算公式,就可以求出整体的差值,然后判断区间。
     * 因为累加和数组不需要删除,所以只需要提供add和查询小于某个值的个数的方法即可。
     * 同时,因为这里需要考虑重复值,我们可以用新增一个字段的方式,来记录真个树的值,方便查询。
     * 这里用新增一个字段allCount的方式,和原来的size字段区分开,这样方便理解改造的过程。
     * 某个节点的allCount就表示以该节点为根的树,所有节点的累加和的个数。
     * 这样,这个节点的值的个数就是allCount - (左子树的allCount) - (右子树的allCount)
     * <br>
     * 过程:
     * 1、新建一个改造的SB树结构,用来插入和查询小于某个值的元素
     * 2、用一个变量sum来记录累计到当前位置的累加和,到了某个位置i,此时的问题就变成了必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
     * 3、从0开始遍历数组,每到一个位置,累加和sun累计当前的值arr[i],然后计算出新的需要判断的边界sum-upper和sum-lower
     * 4、用SB树查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
     * 因为要求的是[sum-upper,sum-lower]这个闭区间的范围,这个范围要想包含sum-upper的边界,就要减去小于sum-upper的值,即不能包含sum-upper,否则就会少算相等的一部分值。
     * 因为SB树查询的就是小于某个树的个数,所以问题就转成了从SB中查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
     * <br>
     * 提交时方法名改为:countRangeSum
     */
    public static int countRangeSumWithSizeBalanced(int[] arr, int lower, int upper) {
        if (arr == null || arr.length == 0 || lower > upper) {
            return 0;
        }
        // SB的改造结构
        SizeBalancedTreeSet sbt = new SizeBalancedTreeSet();
        // 累计和的变量
        long sum = 0;
        // 记录最终的结果
        int ans = 0;
        // 一个数都没有的时候,就已经有一个前缀和累加和为0
        sbt.add(0);
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
            // 计算出新的需要判断的边界sum-upper和sum-lower
            long lowerBound = sum - upper;
            long upperBound = sum - lower + 1;
            // 用SB树查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
            ans += sbt.lessKeySize(upperBound) - sbt.lessKeySize(lowerBound);
            // 将当前的累加和加入到SB树中
            sbt.add(sum);
        }
        return ans;
    }

    /**
     * 改写的SB树,新增一个字段allCount,记录以该节点为根的树,所有节点的累加和的个数。
     */
    private static class SizeBalancedTreeSet {
        // root节点
        private Node root;
        // 用一个set来快速判断某个值有没有添加过
        private Set<Long> set = new HashSet<>();


        /**
         * 右旋
         */
        private Node rightRotate(Node cur) {
            if (cur == null || cur.left == null) {
                return cur;
            }
            // 先记录当前节点的相同的个数
            int sameCount = cur.allCount - cur.left.allCount - (cur.right != null ? cur.right.allCount : 0);
            Node leftNode = cur.left;
            cur.left = leftNode.right;
            leftNode.right = cur;
            // 更新size,旋转对总的size不变,所以此时根节点的size就是原来cur的size
            leftNode.size = cur.size;
            cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
            // 更新allCount,旋转对节点数量不变,所以此时的根节点leftNode的allCount是原来的cur的allCount
            leftNode.allCount = cur.allCount;
            cur.allCount = (cur.left != null ? cur.left.allCount : 0) + sameCount + (cur.right != null ? cur.right.allCount : 0);
            return leftNode;
        }

        /**
         * 左旋
         */
        private Node leftRotate(Node cur) {
            if (cur == null || cur.right == null) {
                return cur;
            }
            int sameCount = cur.allCount - cur.right.allCount - (cur.left != null ? cur.left.allCount : 0);
            Node rightNode = cur.right;
            cur.right = rightNode.left;
            rightNode.left = cur;
            // 更新size
            rightNode.size = cur.size;
            cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
            // 更新allCount
            rightNode.allCount = cur.allCount;
            cur.allCount = sameCount + (cur.right != null ? cur.right.allCount : 0) + (cur.left != null ? cur.left.allCount : 0);
            return rightNode;
        }

        /**
         * 平衡调整
         */
        private Node maintain(Node cur) {
            if (cur == null) {
                return null;
            }
            int leftSize = cur.left != null ? cur.left.size : 0;
            int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
            int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
            int rightSize = cur.right != null ? cur.right.size : 0;
            int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
            int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
            if (leftLeftSize > rightSize) {
                // LL
                cur = rightRotate(cur);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (leftRightSize > rightSize) {
                // LR
                cur.left = leftRotate(cur.left);
                cur = rightRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (rightRightSize > leftSize) {
                // RR
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur = maintain(cur);
            } else if (rightLeftSize > leftSize) {
                // RL
                cur.right = rightRotate(cur.right);
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            }
            return cur;
        }


        /**
         * 递归添加元素
         */
        private Node add(Node cur, long key, boolean contains) {
            if (cur == null) {
                return new Node(key);
            }
            cur.allCount++;
            // key相同,直接返回
            if (key == cur.key) {
                return cur;
            }
            // 递归添加,先判断是否已经存在,如果已经存在,添加的时候只需要更新allCount字段,不需要更新size字段
            if (!contains) {
                cur.size++;
            }
            if (key < cur.key) {
                cur.left = add(cur.left, key, contains);
            } else {
                cur.right = add(cur.right, key, contains);
            }
            return maintain(cur);
        }

        /**
         * 添加元素
         */
        public void add(long key) {
            boolean contains = set.contains(key);
            // 调用递归add方法添加
            this.root = add(this.root, key, contains);
            if (!contains) {
                this.set.add(key);
            }
        }

        /**
         * 获得小于某个key的个数
         */
        public int lessKeySize(long key) {
            Node cur = root;
            int ans = 0;
            while (cur != null) {
                if (key == cur.key) {
                    // 相等,累计上左侧的值即可
                    return ans + (cur.left != null ? cur.left.allCount : 0);
                } else if (key < cur.key) {
                    // 小于的话,从左侧查找
                    cur = cur.left;
                } else {
                    // 累计上左侧和当前的值,往右查找,
                    // 累计左侧和当前的重复值,就是用当前的减去右侧的值
                    ans += cur.allCount - (cur.right != null ? cur.right.allCount : 0);
                    cur = cur.right;
                }
            }
            return ans;
        }

        class Node {
            private long key;
            private Node left;
            private Node right;
            private int size;
            // 以该节点为根的树,所有节点的累加和的个数
            private int allCount;

            public Node(long key) {
                this.key = key;
                this.size = 1;
                this.allCount = 1;
            }
        }
    }

    /**
     * 改写归并排序的方法:
     * 思路:
     * 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
     * 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
     * 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0..j] - sum[0..i-1]也在[lower,upper]范围,
     * 4.所以如果sum[0..j]为x,为了能让sum[0..j]满足[lower,upper]范围,那么sum[0..i-1]必须在[x-upper,x-lower]范围
     * 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
     * 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
     * 7. 用归并排序方法求解,对sum数组进行排序,在分解前,就可以求出arr[0...i]是否满足,即sum[i]是不是在[lower,upper]范围,
     * 8. 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
     * 9. 最后返回所有的个数和,就是求解的个数
     * <br>
     * 过程:
     * 1. 先求前缀和数组sum,sum[i]表示arr[0...i]的累加和
     * 2. 递归处理sum数组,求在sum[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围,结束递归的条件L==R,判断sum[L]是否达标,代表了arr[0...L]这个数组上的累加和是不是达标
     * 3. 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
     * 4. 最后返回所有的个数和,就是求解的个数
     * <br>
     * 提交时方法名改为:countRangeSum
     */
    public static int countRangeSumWithMergeSort(int[] arr, int lower, int upper) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        // 构建前缀和数组,为了防止溢出,要用long类型
        long[] sum = new long[arr.length];
        sum[0] = arr[0];
        for (int i = 1; i < arr.length; i++) {
            sum[i] = sum[i - 1] + arr[i];
        }
        // 递归处理sum数组,求在sum[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围
        return process(sum, 0, sum.length - 1, lower, upper);
    }

    /**
     * 递归处理函数:
     * 求在arr[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围
     * 因为已经求了累加和sum数组,所以直接用传入sum就可以了,不需要再传arr数组
     */
    private static int process(long[] sum, int L, int R, int lower, int upper) {
        if (L == R) {
            // 代表arr[0...L]这个数组上的累加和是不是达标
            return sum[L] >= lower && sum[L] <= upper ? 1 : 0;
        }
        // 递归处理左右两边
        int M = L + ((R - L) >> 1);
        return process(sum, L, M, lower, upper) + process(sum, M + 1, R, lower, upper)
                + merge(sum, L, M, R, lower, upper);
    }

    /**
     * 合并函数:
     * 求在arr[L...M]和arr[M+1...R]合并的过程中,求出多少个累加和在[lower,upper]范围
     * 因为已经求了累加和sum数组,所以直接用传入sum就可以了,不需要再传arr数组
     * 在合并前,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
     */
    public static int merge(long[] sum, int L, int M, int R, int lower, int upper) {
        int ans = 0;
        int windowL = L;
        int windowR = L;
        /**
         * 先求出右组中每个数x,左组中有多少个数,位于[x-upper,x-lower]范围
         * [windowL, windowR)
         * 因为左组和右组都是有序的,可以用滑动窗口的方法,求左组中有多少个数,位于[x-upper,x-lower]范围
         */
        for (int i = M + 1; i <= R; i++) {
            long min = sum[i] - upper;
            long max = sum[i] - lower;
            // 先移动windowR,找到第一个大于max的位置
            while (windowR <= M && sum[windowR] <= max) {
                windowR++;
            }
            // 再移动windowL,找到第一个大于等于min的位置
            while (windowL <= M && sum[windowL] < min) {
                windowL++;
            }
            ans += windowR - windowL;
        }
        // 正常合并
        long[] help = new long[R - L + 1];
        int index = 0;
        int p1 = L;
        int p2 = M + 1;
        while (p1 <= M && p2 <= R) {
            help[index++] = sum[p1] <= sum[p2] ? sum[p1++] : sum[p2++];
        }
        while (p1 <= M) {
            help[index++] = sum[p1++];
        }
        while (p2 <= R) {
            help[index++] = sum[p2++];
        }
        for (int i = 0; i < help.length; i++) {
            sum[L + i] = help[i];
        }
        return ans;
    }

    public static void main(String[] args) {
        // 测试次数
        int testTime = 500000;
        // 数组最大长度
        int maxSize = 100;
        // 数组最大值
        int maxValue = 100;
        boolean succeed = true;
        for (int i = 0; i < testTime; i++) {
            int[] arr = generateRandomArr(maxSize, maxValue);
            int temp1 = (int) (Math.random() * (maxValue + 1)) - (int) (Math.random() * maxValue);
            int temp2 = (int) (Math.random() * (maxValue + 1)) - (int) (Math.random() * maxValue);
            int lower = Math.min(temp1, temp2);
            int upper = Math.max(temp1, temp2);
            int countSum1 = countRangeSumWithSumArr(copyArr(arr), lower, upper);
            int countSum2 = countRangeSumWithSizeBalanced(copyArr(arr), lower, upper);
            int countSum3 = countRangeSumWithMergeSort(copyArr(arr), lower, upper);
            if (countSum1 != countSum2 || countSum1 != countSum3) {
                succeed = false;
                System.out.println("原数组:");
                printArray(arr);
                System.out.printf("lowper:%d,upper:%d,暴力累加和方法:%d,SB方法:%d,归并排序方法:%d \n", lower, upper, countSum1, countSum2, countSum3);
                break;
            }
        }
        System.out.println(succeed ? "successful!" : "error!");
    }

    public static int[] generateRandomArr(int maxSize, int maxValue) {
        // Math.random() -> [0,1) 所有的小数,等概率返回一个
        // Math.random() * N -> [0,N) 所有小数,等概率返回一个
        // (int)(Math.random() * N) -> [0,N-1] 所有的整数,等概率返回一个
        // (int)(Math.random() * (maxSize + 1)) -> [0,maxSize] 所有的整数,等概率返回一个
        int size = (int) (Math.random() * (maxSize + 1));
        int[] arr = new int[size];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) (Math.random() * (maxValue + 1)) - (int) (Math.random() * maxValue);
        }
        return arr;
    }

    public static int[] copyArr(int[] arr) {
        if (arr == null) {
            return null;
        }
        int[] res = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            res[i] = arr[i];
        }
        return res;
    }

    public static void printArray(int[] arr) {
        if (arr == null) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

}

3、题目二:窗口的中位数

  • 题目二:窗口的中位数
  • 给你一个数组 nums,有一个长度为 k 的窗口从最左端滑动到最右端。
  • 窗口中有 k 个数,每次窗口向右移动 1 位。
  • 你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。
  • 中位数是有序序列最中间的那个数。如果序列的长度是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。
  • 测试链接:https://leetcode.cn/problems/sliding-window-median

3.1、使用SBT的方法求解

  • 使用SBT的方法求解
  • 思路:
    • 本题的要求是在一个窗口里面,求中位数,这就需要两个过程:
    • 1、加入和删除某个数字的时候,要排序
    • 2、要能根据下标取到对应位置的值,这样的话,就能求到中位数。
    • 大顶堆和小顶堆能根据元素来维护有序性,但是无法取到指定位置的数,所以无法满足要求,
    • 我们可以根据SBT来改造成这样的结构。
    • 首先,SBT本来就天生支持根据某个index来取值,因为通过其对size的计算,就可以获取到第index的位置,
    • 其次,SBT中的数据是有序的,我们只需要中序遍历数组,就能获得有序的数组。
    • 所以SBT是天然支持这两个要求的,我们需要的是如何将本题目的要求和SBT的结构相匹配。
    • 我们可以将题目中的数组的下标index和值value封装成一个类,
    • 这个类实现Comparable接口,在value不同的时候用value排序,在value相同的时候,用index排序,
    • 这样既实现了排序的问题,也适配了可能出现的重复value的情况。
    • 然后我们在SBT中只维护k个元素,每次都是取其中间的位置的值(如果是奇数取1个,偶数取2个),
    • 然后就可以根据取出的值的value计算出平均数,就是需要的中位数了。
      整体代码如下:
java 复制代码
/**
 * 题目二:窗口的中位数
 * 给你一个数组 nums,有一个长度为 k 的窗口从最左端滑动到最右端。
 * 窗口中有 k 个数,每次窗口向右移动 1 位。
 * 你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。
 * 中位数是有序序列最中间的那个数。如果序列的长度是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。
 * 测试链接:https://leetcode.cn/problems/sliding-window-median
 */
public class Q2_SlidingWindowMedian {

    /**
     * 使用SBT的方法求解
     * 思路:
     * 本题的要求是在一个窗口里面,求中位数,这就需要两个过程:
     * 1、加入和删除某个数字的时候,要排序
     * 2、要能根据下标取到对应位置的值,这样的话,就能求到中位数。
     * 大顶堆和小顶堆能根据元素来维护有序性,但是无法取到指定位置的数,所以无法满足要求,
     * 我们可以根据SBT来改造成这样的结构。
     * 首先,SBT本来就天生支持根据某个index来取值,因为通过其对size的计算,就可以获取到第index的位置,
     * 其次,SBT中的数据是有序的,我们只需要中序遍历数组,就能获得有序的数组。
     * 所以SBT是天然支持这两个要求的,我们需要的是如何将本题目的要求和SBT的结构相匹配。
     * 我们可以将题目中的数组的下标index和值value封装成一个类,
     * 这个类实现Comparable接口,在value不同的时候用value排序,在value相同的时候,用index排序,
     * 这样既实现了排序的问题,也适配了可能出现的重复value的情况。
     * 然后我们在SBT中只维护k个元素,每次都是取其中间的位置的值(如果是奇数取1个,偶数取2个),
     * 然后就可以根据取出的值的value计算出平均数,就是需要的中位数了。
     */
    public static double[] medianSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k <= 0) {
            return new double[0];
        }
        // 新定义一个根据value和index比较的节点类
        class QNode implements Comparable<QNode> {
            private final int index;
            private final int value;

            public QNode(int index, int value) {
                this.index = index;
                this.value = value;
            }

            @Override
            public int compareTo(QNode o) {
                // 先用value排序,后用index排序
                return value != o.value ? Integer.compare(value, o.value)
                        : Integer.compare(index, o.index);
            }
        }
        // 新建SBT结构,并将前k-1个数加入到SBT中
        SizeBalancedTree<QNode> sbt = new SizeBalancedTree<>();
        for (int i = 0; i < k - 1; i++) {
            sbt.add(new QNode(i, nums[i]));
        }
        double[] ans = new double[nums.length - k + 1];
        int index = 0;
        // 从k-1下标开始,获取中位数并加到ans中
        // 因为前面已经加了k-1个数,再加一个数就满足k个数的区间了
        for (int i = k - 1; i < nums.length; i++) {
            sbt.add(new QNode(i, nums[i]));
            // 根据个数计算中位数
            int size = sbt.size();
            if (size % 2 == 0) {
                // 偶数个,取中间的2个数的平均数,getIndex是从0开始的,所以偶数个的上中位数下标为 size/2,下中位数下标为 size/2-1
                QNode downMid = sbt.getIndexKey(size / 2 - 1);
                QNode upMid = sbt.getIndexKey(size / 2);
                ans[index++] = ((double) downMid.value + (double) upMid.value) / 2.0;
            } else {
                // 奇数个,取中间的数
                QNode mid = sbt.getIndexKey(size / 2);
                ans[index++] = mid.value;
            }
            // 删除掉最前面的数,准备下次添加
            sbt.remove(new QNode(i - k + 1, nums[i - k + 1]));
        }
        return ans;
    }


    /**
     * 改写的SB树结构
     * 因为使用了泛型,所以这个结构和平常的SBT没有区别
     */
    static class SizeBalancedTree<K extends Comparable<K>> {
        private Node<K> root;


        public int size() {
            return root == null ? 0 : root.size;
        }

        public boolean containsKey(K key) {
            if (key == null) {
                return false;
            }
            Node<K> lastIndex = findLastIndex(key);
            return lastIndex != null && key.compareTo(lastIndex.key) == 0;
        }

        public void add(K key) {
            if (key == null) {
                return;
            }
            // 不存在加
            if (!containsKey(key)) {
                root = add(root, key);
            }
        }

        public void remove(K key) {
            if (key == null) {
                return;
            }
            if (containsKey(key)) {
                root = delete(root, key);
            }
        }

        /**
         * 获取第index(从0开始)的元素
         */
        public K getIndexKey(int index) {
            if (index < 0 || index >= size()) {
                return null;
            }
            return getIndex(root, index + 1).key;
        }

        private Node<K> rightRotate(Node<K> cur) {
            if (cur == null || cur.left == null) {
                return cur;
            }
            Node<K> leftNode = cur.left;
            cur.left = leftNode.right;
            leftNode.right = cur;
            leftNode.size = cur.size;
            cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
            return leftNode;
        }

        private Node<K> leftRotate(Node<K> cur) {
            if (cur == null || cur.right == null) {
                return cur;
            }
            Node<K> rightNode = cur.right;
            cur.right = rightNode.left;
            rightNode.left = cur;
            rightNode.size = cur.size;
            cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
            return rightNode;
        }

        private Node<K> maintain(Node<K> cur) {
            if (cur == null) {
                return null;
            }
            int leftSize = cur.left != null ? cur.left.size : 0;
            int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
            int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
            int rightSize = cur.right != null ? cur.right.size : 0;
            int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
            int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
            if (leftLeftSize > rightSize) {
                // LL
                cur = rightRotate(cur);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (leftRightSize > rightSize) {
                // LR
                cur.left = leftRotate(cur.left);
                cur = rightRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (rightRightSize > leftSize) {
                // RR
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur = maintain(cur);
            } else if (rightLeftSize > leftSize) {
                // RL
                cur.right = rightRotate(cur.right);
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            }
            return cur;
        }

        private Node<K> findLastIndex(K key) {
            if (root == null) {
                return null;
            }
            Node<K> pre = root;
            Node<K> cur = root;
            while (cur != null) {
                pre = cur;
                if (key.compareTo(cur.key) == 0) {
                    break;
                } else if (key.compareTo(cur.key) < 0) {
                    cur = cur.left;
                } else {
                    cur = cur.right;
                }
            }
            return pre;
        }

        private Node<K> add(Node<K> cur, K key) {
            if (cur == null) {
                return new Node<>(key);
            }
            cur.size++;
            if (key.compareTo(cur.key) < 0) {
                cur.left = add(cur.left, key);
            } else {
                cur.right = add(cur.right, key);
            }
            return maintain(cur);
        }

        private Node<K> delete(Node<K> cur, K key) {
            cur.size--;
            if (key.compareTo(cur.key) < 0) {
                cur.left = delete(cur.left, key);
            } else if (key.compareTo(cur.key) > 0) {
                cur.right = delete(cur.right, key);
            } else {
                // 当前节点就是要删除的节点
                if (cur.left == null && cur.right == null) {
                    cur = null;
                } else if (cur.left == null) {
                    cur = cur.right;
                } else if (cur.right == null) {
                    cur = cur.left;
                } else {
                    // 左右子树都有节点,找到右侧最小的节点
                    // 前一个节点,因为要重新计算size,所以需要有前一个节点的变量
                    Node<K> pre = null;
                    Node<K> des = cur.right;
                    des.size--;
                    while (des.left != null) {
                        pre = des;
                        des = des.left;
                        des.size--;
                    }

                    if (pre != null) {
                        // 删掉des的left,因为pre肯定没有left节点了
                        pre.left = des.right;
                        // 这一句要放到这里,因为如果pre为null,代表cur.right没有左节点,直接接cur.left即可
                        des.right = cur.right;
                    }
                    des.left = cur.left;
                    des.size = des.left.size + (des.right == null ? 0 : des.right.size) + 1;
                    cur = des;
                }
            }
            return cur;
        }

        /**
         * 从当前子树获得第k个节点
         */
        private Node<K> getIndex(Node<K> cur, int kth) {
            if (cur == null) {
                return null;
            }
            if (kth == (cur.left != null ? cur.left.size : 0) + 1) {
                // 要的是左子树+1的节点,也就是当前节点
                return cur;
            } else if (kth <= (cur.left != null ? cur.left.size : 0)) {
                // 要的是左子树的第kth个节点
                return getIndex(cur.left, kth);
            } else {
                // 要的是右子树的第k个节点,需要转换右侧的索引
                return getIndex(cur.right, kth - (cur.left != null ? cur.left.size : 0) - 1);
            }
        }

        /**
         * SBT树的内部节点
         */
        static class Node<K extends Comparable<K>> {
            private final K key;
            private int size;
            private Node<K> left;
            private Node<K> right;

            public Node(K key) {
                this.key = key;
                this.size = 1;
            }
        }
    }

    public static void main(String[] args) {
        int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
        int k = 3;
        double[] ans = medianSlidingWindow(nums, k);
        for (double v : ans) {
            System.out.println(v);
        }
    }

}

4、题目三:设计一个保持时序的列表

  • 题目三:设计一个保持时序的列表
  • 设计一个结构包含如下两个方法:
  • void add(int index, int num):把num加入到index位置
  • int get(int index) :取出index位置的值
  • void remove(int index) :把index位置上的值删除
  • 要求三个方法时间复杂度O(logN)

4.1、使用SBT的方法求解

  • 用SBT改写的支持用所以增加、查询、删除操作的方法
  • 思路:
    • SB树是支持index查询的,但是不支持index插入和删除,这是因为SB树是根据key的值来做比较的,
    • 如果我们用插入顺序来做逻辑的key,就可以直接复用size字段来做已经有的节点的index,
    • 在插入和删除的时候,根据传入的index字段和size进行对比,就能计算出对应的逻辑key,
    • 同时,因为是用的逻辑key做的比较,相同的value是作为不同的节点存在于树中的,所以改写的SBT就天然支持重复元素。
    • 具体参照实现的类SbtList
java 复制代码
    /**
     * 用SBT改写的支持用所以增加、查询、删除操作的方法
     * 思路:
     * SB树是支持index查询的,但是不支持index插入和删除,这是因为SB树是根据key的值来做比较的,
     * 如果我们用插入顺序来做逻辑的key,就可以直接复用size字段来做已经有的节点的index,
     * 在插入和删除的时候,根据传入的index字段和size进行对比,就能计算出对应的逻辑key,
     * 同时,因为是用的逻辑key做的比较,相同的value是作为不同的节点存在于树中的,所以改写的SBT就天然支持重复元素。
     * 具体参照实现的类SbtList
     */
    public static class SbtList<V> {
        private Node<V> root;

        public void add(int index, V num) {
            Node<V> node = new Node<>(num);
            if (root == null) {
                root = node;
            } else {
                if (index <= root.size) {
                    // 只有符合条件的index才能添加
                    root = add(root, index, node);
                }
            }
        }

        public V get(int index) {
            if (index >= 0 && size() > index) {
                Node<V> ans = get(root, index);
                return ans.value;
            }
            return null;
        }

        public void remove(int index) {
            if (index >= 0 && size() > index) {
                root = delete(root, index);
            }
        }

        public int size() {
            return root == null ? 0 : root.size;
        }


        /**
         * 递归给当前节点cur的子树添加index(从0开始)位置的node节点
         * 当要替换掉一个已经存在的index位置的时候,新加入的节点实际上还是会加到合适的最底层,
         * 但是由于新加入的节点的存在,在中序遍历中会占据index的位置,让后面的节点往后移动,所以自然是符合时序的
         */
        private Node<V> add(Node<V> cur, int index, Node<V> node) {
            // 到最后为null的节点了,直接将node放到这个位置
            if (cur == null) {
                return node;
            }
            cur.size++;
            // 利用size计算出当前cur节点的左侧和本身的长度
            int leftAndHeadSize = (cur.left != null ? cur.left.size : 0) + 1;
            // 用index判断需要插入到什么位置
            if (index < leftAndHeadSize) {
                // 插入到左子树
                cur.left = add(cur.left, index, node);
            } else {
                // 插入到右子树,需要重新计算下个位置的坐标,
                cur.right = add(cur.right, index - leftAndHeadSize, node);
            }
            // 维护平衡性
            return maintain(cur);

        }

        /**
         * 递归删除当前节点cur的子树中index(从0开始)位置的节点
         */
        private Node<V> delete(Node<V> cur, int index) {
            if (cur == null) {
                return null;
            }
            cur.size--;
            // 当前头结点的序号
            int rootIndex = (cur.left != null ? cur.left.size : 0);
            if (index != rootIndex) {
                // 删除的不是当前的cur节点的数,按照序号递归即可
                if (index < rootIndex) {
                    cur.left = delete(cur.left, index);
                } else {
                    cur.right = delete(cur.right, index - rootIndex - 1);
                }
                return cur;
            }
            // 删除的是当前的cur节点
            if (cur.left == null && cur.right == null) {
                return null;
            }
            if (cur.left == null) {
                return cur.right;
            }
            if (cur.right == null) {
                return cur.left;
            }
            // 两边都有子树,找到右侧最小的替换
            Node<V> pre = null;
            Node<V> des = cur.right;
            des.size--;
            while (des.left != null) {
                pre = des;
                des = des.left;
                des.size--;
            }
            if (pre != null) {
                pre.left = des.right;
                des.right = cur.right;
            }
            des.left = cur.left;
            des.size = 1 + (des.left != null ? des.left.size : 0) + (des.right != null ? des.right.size : 0);
            return des;
        }

        /**
         * 递归查询当前节点cur的子树中index(从0开始)位置的节点
         */
        private Node<V> get(Node<V> cur, int index) {
            if (cur == null) {
                return null;
            }
            int leftSize = cur.left != null ? cur.left.size : 0;
            if (index < leftSize) {
                return get(cur.left, index);
            } else if (index == leftSize) {
                return cur;
            } else {
                return get(cur.right, index - leftSize - 1);
            }
        }

        private Node<V> rightRotate(Node<V> cur) {
            if (cur == null || cur.left == null) {
                return cur;
            }
            Node<V> leftNode = cur.left;
            cur.left = leftNode.right;
            leftNode.right = cur;
            leftNode.size = cur.size;
            cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
            return leftNode;
        }

        private Node<V> leftRotate(Node<V> cur) {
            if (cur == null || cur.right == null) {
                return cur;
            }
            Node<V> rightNode = cur.right;
            cur.right = rightNode.left;
            rightNode.left = cur;
            rightNode.size = cur.size;
            cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
            return rightNode;
        }

        private Node<V> maintain(Node<V> cur) {
            if (cur == null) {
                return null;
            }
            int leftSize = cur.left != null ? cur.left.size : 0;
            int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
            int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
            int rightSize = cur.right != null ? cur.right.size : 0;
            int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
            int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
            if (leftLeftSize > rightSize) {
                // LL
                cur = rightRotate(cur);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (leftRightSize > rightSize) {
                // LR
                cur.left = leftRotate(cur.left);
                cur = rightRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (rightRightSize > leftSize) {
                // RR
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur = maintain(cur);
            } else if (rightLeftSize > leftSize) {
                // RL
                cur.right = rightRotate(cur.right);
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            }
            return cur;
        }

        /**
         * 节点类
         * 因为是按照插入顺序来做逻辑key的,可以复用size字段,
         * 所以节点就没有必要再存储一个key了,只需要存储value即可。
         */
        static class Node<V> {
            private V value;
            private Node<V> left;
            private Node<V> right;
            private int size;

            public Node(V value) {
                this.value = value;
                this.size = 1;
            }
        }
    }

整体代码和测试如下:

java 复制代码
import java.util.ArrayList;

/**
 * 题目三:设计一个保持时序的列表
 * 设计一个结构包含如下两个方法:
 * void add(int index, int num):把num加入到index位置
 * int get(int index) :取出index位置的值
 * void remove(int index) :把index位置上的值删除
 * 要求三个方法时间复杂度O(logN)
 */
public class Q3_AddRemoveGetWithIndex {
    /**
     * 用SBT改写的支持用所以增加、查询、删除操作的方法
     * 思路:
     * SB树是支持index查询的,但是不支持index插入和删除,这是因为SB树是根据key的值来做比较的,
     * 如果我们用插入顺序来做逻辑的key,就可以直接复用size字段来做已经有的节点的index,
     * 在插入和删除的时候,根据传入的index字段和size进行对比,就能计算出对应的逻辑key,
     * 同时,因为是用的逻辑key做的比较,相同的value是作为不同的节点存在于树中的,所以改写的SBT就天然支持重复元素。
     * 具体参照实现的类SbtList
     */
    public static class SbtList<V> {
        private Node<V> root;

        public void add(int index, V num) {
            Node<V> node = new Node<>(num);
            if (root == null) {
                root = node;
            } else {
                if (index <= root.size) {
                    // 只有符合条件的index才能添加
                    root = add(root, index, node);
                }
            }
        }

        public V get(int index) {
            if (index >= 0 && size() > index) {
                Node<V> ans = get(root, index);
                return ans.value;
            }
            return null;
        }

        public void remove(int index) {
            if (index >= 0 && size() > index) {
                root = delete(root, index);
            }
        }

        public int size() {
            return root == null ? 0 : root.size;
        }


        /**
         * 递归给当前节点cur的子树添加index(从0开始)位置的node节点
         * 当要替换掉一个已经存在的index位置的时候,新加入的节点实际上还是会加到合适的最底层,
         * 但是由于新加入的节点的存在,在中序遍历中会占据index的位置,让后面的节点往后移动,所以自然是符合时序的
         */
        private Node<V> add(Node<V> cur, int index, Node<V> node) {
            // 到最后为null的节点了,直接将node放到这个位置
            if (cur == null) {
                return node;
            }
            cur.size++;
            // 利用size计算出当前cur节点的左侧和本身的长度
            int leftAndHeadSize = (cur.left != null ? cur.left.size : 0) + 1;
            // 用index判断需要插入到什么位置
            if (index < leftAndHeadSize) {
                // 插入到左子树
                cur.left = add(cur.left, index, node);
            } else {
                // 插入到右子树,需要重新计算下个位置的坐标,
                cur.right = add(cur.right, index - leftAndHeadSize, node);
            }
            // 维护平衡性
            return maintain(cur);

        }

        /**
         * 递归删除当前节点cur的子树中index(从0开始)位置的节点
         */
        private Node<V> delete(Node<V> cur, int index) {
            if (cur == null) {
                return null;
            }
            cur.size--;
            // 当前头结点的序号
            int rootIndex = (cur.left != null ? cur.left.size : 0);
            if (index != rootIndex) {
                // 删除的不是当前的cur节点的数,按照序号递归即可
                if (index < rootIndex) {
                    cur.left = delete(cur.left, index);
                } else {
                    cur.right = delete(cur.right, index - rootIndex - 1);
                }
                return cur;
            }
            // 删除的是当前的cur节点
            if (cur.left == null && cur.right == null) {
                return null;
            }
            if (cur.left == null) {
                return cur.right;
            }
            if (cur.right == null) {
                return cur.left;
            }
            // 两边都有子树,找到右侧最小的替换
            Node<V> pre = null;
            Node<V> des = cur.right;
            des.size--;
            while (des.left != null) {
                pre = des;
                des = des.left;
                des.size--;
            }
            if (pre != null) {
                pre.left = des.right;
                des.right = cur.right;
            }
            des.left = cur.left;
            des.size = 1 + (des.left != null ? des.left.size : 0) + (des.right != null ? des.right.size : 0);
            return des;
        }

        /**
         * 递归查询当前节点cur的子树中index(从0开始)位置的节点
         */
        private Node<V> get(Node<V> cur, int index) {
            if (cur == null) {
                return null;
            }
            int leftSize = cur.left != null ? cur.left.size : 0;
            if (index < leftSize) {
                return get(cur.left, index);
            } else if (index == leftSize) {
                return cur;
            } else {
                return get(cur.right, index - leftSize - 1);
            }
        }

        private Node<V> rightRotate(Node<V> cur) {
            if (cur == null || cur.left == null) {
                return cur;
            }
            Node<V> leftNode = cur.left;
            cur.left = leftNode.right;
            leftNode.right = cur;
            leftNode.size = cur.size;
            cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
            return leftNode;
        }

        private Node<V> leftRotate(Node<V> cur) {
            if (cur == null || cur.right == null) {
                return cur;
            }
            Node<V> rightNode = cur.right;
            cur.right = rightNode.left;
            rightNode.left = cur;
            rightNode.size = cur.size;
            cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
            return rightNode;
        }

        private Node<V> maintain(Node<V> cur) {
            if (cur == null) {
                return null;
            }
            int leftSize = cur.left != null ? cur.left.size : 0;
            int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
            int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
            int rightSize = cur.right != null ? cur.right.size : 0;
            int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
            int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
            if (leftLeftSize > rightSize) {
                // LL
                cur = rightRotate(cur);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (leftRightSize > rightSize) {
                // LR
                cur.left = leftRotate(cur.left);
                cur = rightRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (rightRightSize > leftSize) {
                // RR
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur = maintain(cur);
            } else if (rightLeftSize > leftSize) {
                // RL
                cur.right = rightRotate(cur.right);
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            }
            return cur;
        }

        /**
         * 节点类
         * 因为是按照插入顺序来做逻辑key的,可以复用size字段,
         * 所以节点就没有必要再存储一个key了,只需要存储value即可。
         */
        static class Node<V> {
            private V value;
            private Node<V> left;
            private Node<V> right;
            private int size;

            public Node(V value) {
                this.value = value;
                this.size = 1;
            }
        }
    }

    // 通过以下这个测试,
    // 可以很明显的看到LinkedList的插入、删除、get效率不如SbtList
    // LinkedList需要找到index所在的位置之后才能插入或者读取,时间复杂度O(N)
    // SbtList是平衡搜索二叉树,所以插入或者读取时间复杂度都是O(logN)
    public static void main(String[] args) {
        // 功能测试
        int test = 50000;
        int max = 1000000;
        boolean pass = true;
        ArrayList<Integer> list = new ArrayList<>();
        SbtList<Integer> sbtList = new SbtList<>();
        for (int i = 0; i < test; i++) {
            if (list.size() != sbtList.size()) {
                pass = false;
                break;
            }
            if (list.size() > 1 && Math.random() < 0.5) {
                int removeIndex = (int) (Math.random() * list.size());
                list.remove(removeIndex);
                sbtList.remove(removeIndex);
            } else {
                int randomIndex = (int) (Math.random() * (list.size() + 1));
                int randomValue = (int) (Math.random() * (max + 1));
                list.add(randomIndex, randomValue);
                sbtList.add(randomIndex, randomValue);
            }
        }
        for (int i = 0; i < list.size(); i++) {
            if (!list.get(i).equals(sbtList.get(i))) {
                pass = false;
                break;
            }
        }
        System.out.println("功能测试是否通过 : " + pass);

        // 性能测试
        test = 500000;
        list = new ArrayList<>();
        sbtList = new SbtList<>();
        long start = 0;
        long end = 0;

        start = System.currentTimeMillis();
        for (int i = 0; i < test; i++) {
            int randomIndex = (int) (Math.random() * (list.size() + 1));
            int randomValue = (int) (Math.random() * (max + 1));
            list.add(randomIndex, randomValue);
        }
        end = System.currentTimeMillis();
        System.out.println("ArrayList插入总时长(毫秒) : " + (end - start));

        start = System.currentTimeMillis();
        for (int i = 0; i < test; i++) {
            int randomIndex = (int) (Math.random() * (i + 1));
            list.get(randomIndex);
        }
        end = System.currentTimeMillis();
        System.out.println("ArrayList读取总时长(毫秒) : " + (end - start));

        start = System.currentTimeMillis();
        for (int i = 0; i < test; i++) {
            int randomIndex = (int) (Math.random() * list.size());
            list.remove(randomIndex);
        }
        end = System.currentTimeMillis();
        System.out.println("ArrayList删除总时长(毫秒) : " + (end - start));

        start = System.currentTimeMillis();
        for (int i = 0; i < test; i++) {
            int randomIndex = (int) (Math.random() * (sbtList.size() + 1));
            int randomValue = (int) (Math.random() * (max + 1));
            sbtList.add(randomIndex, randomValue);
        }
        end = System.currentTimeMillis();
        System.out.println("SbtList插入总时长(毫秒) : " + (end - start));

        start = System.currentTimeMillis();
        for (int i = 0; i < test; i++) {
            int randomIndex = (int) (Math.random() * (i + 1));
            sbtList.get(randomIndex);
        }
        end = System.currentTimeMillis();
        System.out.println("SbtList读取总时长(毫秒) :  " + (end - start));

        start = System.currentTimeMillis();
        for (int i = 0; i < test; i++) {
            int randomIndex = (int) (Math.random() * sbtList.size());
            sbtList.remove(randomIndex);
        }
        end = System.currentTimeMillis();
        System.out.println("SbtList删除总时长(毫秒) :  " + (end - start));

    }
}

后记

个人学习总结笔记,不能保证非常详细,轻喷

相关推荐
goodlook01232 小时前
监控平台搭建-监控指标展示-Grafana篇(五)
java·算法·docker·grafana·prometheus
这是个栗子2 小时前
前端开发中的常用工具函数(持续更新中...)
前端·javascript·算法
wearegogog1232 小时前
基于MATLAB的微光图像增强实现方案
算法
断剑zou天涯2 小时前
【算法笔记】有序表——SB树
笔记·算法
曾几何时`2 小时前
滑动窗口(十五)2962. 统计最大元素出现至少 K 次的子数组(越长越合法型)
数据结构·算法
AI视觉网奇2 小时前
NVIDIA 生成key
笔记·nvidia
代码游侠2 小时前
应用——SQLite3 C 编程学习
linux·服务器·c语言·数据库·笔记·网络协议·sqlite
究极无敌暴龙战神X2 小时前
机器学习相关
人工智能·算法·机器学习
会思考的猴子2 小时前
UE5 笔记二 GameplayAbilitySystem Attributes & Effects
笔记·ue5