[2080. 区间内查询数字的频率 - 力扣竞赛](读秒绝杀....)

547 阅读3分钟

这是268场周赛的第三题,读秒绝杀了这一题。玩的就是心理承受能力,呵呵呵....

image-20211122220848013.png

image-20211122225205459.png

题目:

请你设计一个数据结构,它能求出给定子数组内一个给定值的 频率 。

子数组中一个值的 频率 指的是这个子数组中这个值的出现次数。

请你实现 RangeFreqQuery 类:

RangeFreqQuery(int[] arr) 用下标从 0 开始的整数数组 arr 构造一个类的实例。
int query(int left, int right, int value) 返回子数组 arr[left...right] 中 value 的 频率 。
一个 子数组 指的是数组中一段连续的元素。arr[left...right] 指的是 nums 中包含下标 left 和 right 在内 的中间一段连续元素。

思路:

总体来说题目还是中规中矩的。前两道题做完还剩一个多小时看这第三题。。。花了比较长时间,主要是想到了几种方法,各种尝试过发现也不行,包括:树状数组,前缀和,dp...然后就浪费了不少时间了。

一开始看到多次区间查询,第一反应是树状数组或者线段树,无奈水平不够在这想了半天没想出来怎么改造,不过确实是可以用树状数组来做的,附上其他大佬用树状数组的题解[C++] 树状数组解法 - 区间内查询数字的频率 - 力扣(LeetCode) (leetcode-cn.com)

然后观察题目例子:

输入:
["RangeFreqQuery", "query", "query"]
[[[12, 33, 4, 56, 22, 2, 34, 33, 22, 12, 34, 56]], [1, 2, 4], [0, 11, 33]]
输出:
[null, 1, 2]

解释:
RangeFreqQuery rangeFreqQuery = new RangeFreqQuery([12, 33, 4, 56, 22, 2, 34, 33, 22, 12, 34, 56]);
rangeFreqQuery.query(1, 2, 4); // 返回 1 。4 在子数组 [33, 4] 中出现 1 次。
rangeFreqQuery.query(0, 11, 33); // 返回 2 。33 在整个子数组中出现 2 次。

发现用map来记录每个数字出现的位置是个不错的想法,其中key是每个数字,值是一个list,记录了这个数字出现的下标。

这样的map有两个好处:第一,可以迅速判断出查询的value是否出现过,没有出现过可以直接返回0。第二如果出现过至少可以遍历整个map就能看给定的范围内有多少个value。

当然,直接遍历看数据量肯定会超时。容易想到,可以用二分来减少搜索的复杂度。经过一番测试,发现其实对于左下标来说,就是找到list中第一个大于等于left的下标,比如list=[1,7],left = 0,那么就返回0。

对于右下标,就是找到list中第一个小于等于right的下标。

比赛中的最后“绝杀”的代码如下:

另外,c++是自带有lower_bound,upper_bound方法的。java的话就需要自己写了。

class RangeFreqQuery {

    private Map<Integer, List<Integer>> count = new HashMap<>();

    public RangeFreqQuery(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            List<Integer> counts = count.getOrDefault(arr[i], new ArrayList<>());
            counts.add(i);
            count.put(arr[i], counts);
        }
    }

    public int query(int left, int right, int value) {
        if (!count.containsKey(value)) {
            return 0;
        }
        List<Integer> counts = count.get(value);
        if (right < counts.get(0)) {
            return 0;
        }
        if (left > counts.get(counts.size() - 1)) {
            return 0;
        }
        int leftIndex =  findLeft(left, counts);

        int rightIndex = findRight(right, counts);

        return rightIndex - leftIndex + 1;
    }

    // 右边的话是找到list中第一个小于等于right的下标
    private int findRight(int right, List<Integer> counts) {
        int l1 = 0, r1 = counts.size() - 1;
        while (l1 < r1) {
            int mid = (r1 - l1 + 1) / 2 + l1;
            if (counts.get(mid) > right) {
                r1 = mid - 1;
            } else {
                l1 = mid;
            }
        }
        return l1;
    }

    // 左边的话是找到list中第一个大于等于left的下标,比如list=[1,7],left = 0,那么就返回0
    private int findLeft(int left, List<Integer> counts) {
        int l1 = 0, r1 = counts.size() - 1;
        while (l1 < r1) {
            int mid = (r1 - l1) / 2 + l1;
            if (counts.get(mid) < left) {
                l1 = mid + 1;
            } else {
                r1 = mid;
            }
        }
        return l1;
    }

}

错误了两次是因为需要加如下两个特殊情况的判断:

  if (right < counts.get(0)) {
        return 0;
    }
    if (left > counts.get(counts.size() - 1)) {
        return 0;
    }