这是268场周赛的第三题,读秒绝杀了这一题。玩的就是心理承受能力,呵呵呵....
题目:
请你设计一个数据结构,它能求出给定子数组内一个给定值的 频率 。
子数组中一个值的 频率 指的是这个子数组中这个值的出现次数。
请你实现 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;
}