[leetcode 周赛 149] 1157 子数组中占绝大多数的元素
1157 Online Majority Element In Subarray 子数组中占绝大多数的元素
描述
实现一个 MajorityChecker
的类,它应该具有下述几个 API
:
-
MajorityChecker(int[] arr)
会用给定的数组arr
来构造一个MajorityChecker
的实例。 -
int query(int left, int right, int threshold)
有这么几个参数:0 <= left <= right < arr.length
表示数组arr
的子数组的长度。2 * threshold > right - left + 1
,也就是说阈值threshold
始终比子序列长度的一半还要大。
每次查询query(...)
会返回在arr[left]
,arr[left+1]
, ...,arr[right]
中至少出现阈值次数threshold
的元素,如果不存在这样的元素,就返回-1
。
-
示例:
MajorityChecker majorityChecker = new MajorityChecker([1,1,2,2,1,1]);
majorityChecker.query(0,5,4); // 返回 1
majorityChecker.query(0,3,3); // 返回 -1
majorityChecker.query(2,3,2); // 返回 2
- 提示:
1 <= arr.length <= 20000
1 <= arr[i] <= 20000
对于每次查询,0 <= left <= right < len(arr)
对于每次查询,2 * threshold > right - left + 1
查询次数最多为 10000
思路
-
首先读题:
- 查询
query
需要拥有一个出现次数超过查询长度一半
的元素
众数
(出现次数超过总数一半的元素)思想: 拥有众数的数组, 数组内元素两两对消
(元素不相等)或两两相融
(元素相等), 剩下的必定是众数 - 查询次数上限
10000
, 需要保证查询速率 - 阈值次数
threshold
, 在查询范围内, 众数出现次数获取
可以使用二分查找
左右边界
- 查询
-
可以使用
动态规划
的思想:- 状态转移公式
l
查询左边界 r
查询右边界 val
查询区域众数 cnt
众数在查询区域出现次数
- 子问题合并
两子问题返回众数相等, 则出现次数相加, 不等则出现次数大减小
以此思想可以建立线段树
存储数据来加速查询速率
代码实现
使用线段树提前计算区域众数来加速查询
// 通过线段树来存储某些区域的众数和计数 以此来加速查询
class MajorityChecker {
// idx 存储数组出现元素种类 以及该元素下标索引
// [1, 2, 2, 1, 1, 2] --> [[1 : 0, 3, 4], [2 : 1, 2, 5]]
HashMap<Integer, ArrayList<Integer>> idx;
// 线段树的根节点
SegTreeNode root;
// key 所查找的区域众数
// count 所查找的区域众数出现次数
// 注意: 数组元素范围不包括0
int key=0, count=0;
/**
* 初始化
* 元素索引表idx 和 线段树root
*
* @param arr 被查询数组
* */
public MajorityChecker(int[] arr) {
idx = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
if (!idx.containsKey(arr[i]))
idx.put(arr[i], new ArrayList<Integer>());
idx.get(arr[i]).add(i);
}
root = buildTree(arr, 0, arr.length-1);
}
/**
* 查询区域众数 是否超过阈值
*
* @param left 查询区域左边界
* @param right 查询区域右边界
* @param threshold 用来判断众数的出现次数阈值
* @return key/-1 如果所查询众数key的查询区域出现次数超过阈值threshold则返回key, 否则返回-1
* */
public int query(int left, int right, int threshold) {
// 初始化 所查询众数key 及辅助判断的计数count
key = 0; count = 0;
// 查询线段树
searchSegTree(root, left, right);
// 如果查询区域没有众数 即key没被更改
// 或者
// 所查询出来的众数 在原数组中根本没有超出阈值的能力
if (key == 0 || idx.get(key).size() < threshold) return -1;
// 上确界 排序数组中 第一个大于right的下标
int r = upper_bound(idx.get(key), right);
// 下确界 排序数组中 第一个大于等于left的下标
int l = lower_bound(idx.get(key), left);
count = r - l;
return count >= threshold ? key : -1;
}
// 排序数组中 第一个大于tar的下标
int upper_bound(List<Integer> list, int tar) {
int l = 0, r = list.size();
while (l < r) {
int mid = l + (r-l)/2;
if (list.get(mid) <= tar) l = mid+1;
else r = mid;
}
return l;
}
// 排序数组中 第一个大于等于tar的下标
int lower_bound(List<Integer> list, int tar) {
int l = 0, r = list.size()-1;
while (l < r) {
int mid = l + (r-l)/2;
if (list.get(mid) < tar) l = mid+1;
else r = mid;
}
return l;
}
/**
* 构建线段树
*
* @param arr 被构建数组
* @param l 构建节点的左值 表示查询区域左边界
* @param r 构建节点的右值 表示查询区域右边界
* @return 以构建完成的线段树节点
* */
private SegTreeNode buildTree(int[] arr, int l, int r) {
if (l > r) return null;
// 初始一个线段树节点
SegTreeNode root = new SegTreeNode(l, r);
// 叶子节点
if (l == r) {
// 众数就是当前值 计数为1
root.val = arr[l]; root.count = 1;
return root;
}
int mid = (l+r)/2;
// 构建左子节点
root.left = buildTree(arr, l, mid);
// 构建右子节点
root.right = buildTree(arr, mid+1, r);
// 整合父节点
makeRoot(root);
return root;
}
/**
* 整合一个父节点
*
* @param root 被整合节点
* */
private void makeRoot(SegTreeNode root) {
if (null == root) return;
// 如果该节点有左子节点 该节点的值"先"等于左子节点
if (root.left != null) {
root.val = root.left.val;
root.count = root.left.count;
}
// 如果该节点还有右子节点 融合父节点和子节点
if (root.right != null) {
if (root.val == root.right.val)
root.count = root.count + root.right.count;
else {
if (root.count >= root.right.count)
root.count = root.count - root.right.count;
else {
root.val = root.right.val;
root.count = root.right.count - root.count;
}
}
}
}
/**
* 查询线段树
*
* @param root 被查询节点
* @param l 需要查询的范围左边界
* @param r 需要查询的范围右边界
* */
private void searchSegTree(SegTreeNode root, int l, int r) {
if (null==root | l > r) return;
if (root.l > r | root.r < l) return;
// 当查询边界覆盖节点边界 该节点就是查询区域
if (l <= root.l && root.r <= r) {
if (key == root.val) count += root.count;
else if (count <= root.count) {
key = root.val;
count = root.count - count;
} else {
count = count - root.count;
}
return;
}
int mid = (root.r + root.l)/2;
// root.l <= l <= mid 左节点也可以是查询区域
if (l <= mid) {
searchSegTree(root.left, l, r);
}
// mid+1 <= r <= root.r 右节点也可以是查询区域
if (r >= mid+1) {
searchSegTree(root.right, l, r);
}
}
/**
* 线段树节点 类
* 用于存储区域边界/众数和计数 并且连接下一节点
* */
static class SegTreeNode {
// l 所存储区域左边界
// r 所存储区域右边界
// val 所存储区域众数
// count 所存储区域众数计数(出现次数)
int l, r, val, count;
SegTreeNode left, right;
SegTreeNode(int l, int r) {
this.l = l; this.r = r;
val = 0; count = 0;
left = null; right = null;
}
}
}