347. 前 K 个高频元素
题目描述
给你一个整数数组
nums
和一个整数k
,请你返回其中出现频率前k
高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
解法选择:
- 优先队列
- 求
top k 问题
,优先队列是一把好手
- 求
优先队列
- 底层基于堆实现。堆是一种完全二叉树,其父节点大于(或小于)所有子节点,即大根堆(或小根堆)
- 每次增/删后,保持栈顶元素是 max(或 min)的
- 仅保持局部是有序的,并不是全部有序
注意:
到底选用大顶堆还是小顶堆?
- 一开始我就想也没想,题目要求 top k,那“铁定”大顶堆啊。结果,就是直接翻车 WA,调试发现不对
- 后来一想如果选大顶堆的话,那么,每次出队列弹出的都是max的元素,必然无法使得保留在优先队列中的最终k个是top k(人家都被弹了,还保留个 der 的 top k)
- 所以,铁定的选择小顶堆。每次将当前元素 value 和当前队头元素对应的 value 比较,如果大于之,则出队;否则,啥也不管
思路:
- 使用 map 记录nums中元素出现的个数;
- key:nums[i];value:nums[i] 出现的次数
- 将 map 中前 k 个元素的
key
存入 PriorityQueue; - 将 map 中剩余的 size - k 个元素,与队头元素
peek
比较:- 如果 map 中当前元素的 value 大于 peek 在 map 中的 value,则将当前元素 key 入队;
- 否则,不做任何操作。
- 倒序(因为每次取得都是最小的)从小顶堆中取出k个元素,作为最终结果
- 但题目,其实没有要求输出顺序(扩展一下而已)
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 记录nums中元素出现的个数
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i : nums) {
if (!map.containsKey(i)) {
map.put(i, 1);
} else {
map.put(i, map.get(i) + 1);
}
}
// 优先队列(小顶堆k个元素)
Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return map.get(a) - map.get(b); // // 排序规则(出现次数升序)
}
});
// 遍历 map,用小顶堆保存频率最大的k个元素
for (Integer key : map.keySet()) {
if (queue.size() < k) { // 堆里小于k个元素时候,直接填入
queue.add(key);
} else if (map.get(key) > map.get(queue.peek())) { // 核心:保证小根堆里面只有k个元素,并且当前队列元素是最大的
queue.remove();
queue.add(key);
}
}
// 倒序(因为每次取得都是最小的)从小顶堆中取出k个元素,作为最终结果
// 但题目,其实没有要求输出顺序(扩展一下而已)
int cnt = 0; // ans的当前位置
int[] ans = new int[k]; // 最终答案
for (int i = k - 1; i >= 0; i--) {
ans[i] = queue.poll();
}
return ans;
}
}
703. 数据流中的第 K 大元素
题目描述
设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。
请实现KthLargest
类:
- KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
- int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。
刚才 347. 前 K 个高频元素 求top k 问题
,优先队列(即,堆)是一把好手。而本题 仍然需要用到堆
。
那么到底用 “大顶堆” 还是 “小顶堆” 呢 ?
- 答:
小顶堆
。构造一个小顶堆,堆中维护 k 个元素,则当前堆顶为这 k 个元素的最小值,也即第 k 大元素.
add(int val)
- 由于可能出现 nums 中元素个数小于 k 的情况,如果当前 堆中还不够 k 个元素,则直接把 val 插入,返回新的堆顶元素,即可;
- 否则,如果 当前元素比堆顶元素(即,此时的第 k 大元素)大,则将当前堆顶元素弹出,并将 val 元素插入,val 插入后 新的堆顶元素就是第 k 大元素;
- 否则,即当前元素比堆顶元素(即第k大元素)小 时,则此时堆顶元素即为第 k 大元素,不用做任何操作
- 最终,堆顶元素就是第 k 大元素
class KthLargest {
int k;
int[] nums;
PriorityQueue<Integer> queue;
public KthLargest(int k, int[] nums) {
this.k = k;
this.nums = nums;
// 小根堆。堆中维护 k 个元素,则当前堆顶为这 k 个元素的最小值,也即第 k 大元素
queue = new PriorityQueue<Integer>((o1, o2) -> o1 - o2);
/**
// 前 k 个元素
for (int i = 0; i < k && i < nums.length; i++) {
queue.offer(nums[i]);
}
// 后 nums.length - k 个
for (int i = k; i < nums.length; i++) {
if (nums[i] > queue.peek()) { // 当前元素比 最小堆的堆顶元素大,则插入,并将堆顶元素删除
queue.poll();
queue.offer(nums[i]);
}
}
*/
// 小顶堆 初始化(这样写 比上面两个 for 初始化更简便)
for (int i = 0; i < nums.length; i++) {
add(nums[i]);
}
}
public int add(int val) {
// 如果当前 堆中还不够 k 个元素,则直接把 val 插入,返回新的堆顶元素,即可
if (queue.size() < k) {
queue.offer(val);
} else if (val > queue.peek()) { // 当前元素比堆顶元素(即,此时的第 k 大元素)大,则将当前堆顶元素弹出,并将 val 元素插入,val 插入后 新的堆顶元素就是第 k 大元素
queue.poll(); // 之前的堆顶元素弹出
queue.offer(val); // 插入 val
}
// 当前元素比堆顶元素(即第k大元素)小,则此时堆顶元素即为第 k 大元素,不用做任何操作
return queue.peek(); // 当前堆顶元素就是第 k 大元素
}
}
313. 超级丑数
题目描述
超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 primes 中。
给你一个整数 n 和一个整数数组 primes ,返回第 n 个 超级丑数 。
题目数据保证第 n 个 超级丑数 在 32-bit 带符号整数范围内。
解法选择:
- 优先队列 + set 去重
思路:
- 由于超级丑数 的所有质因数都出现在质数数组
primes
中,所以数数组primes
中两两元素乘积 必然是 超级丑数; - 所以每次取当前已有的 min 丑数,并与
primes
所有元素相乘,即为下一批 超级丑数(可能重复,所以要用 set 去重) - 每次取每次
取当前已有的 min 丑数
进行下一步操作,而最小堆
(优先队列)在这方面效率较高
class Solution {
public int nthSuperUglyNumber(int n, int[] primes) {
int res = -1;
PriorityQueue<Long> minHead = new PriorityQueue<>(); // 小根堆
Set<Long> set = new HashSet<>();
minHead.offer(1L);
while ((n--) > 0) {
// System.out.println("minHead = " + minHead);
long top = minHead.poll();
res = (int)top; // 记录当前小根堆队头元素,即min
for (int prime : primes) {
// 去重
if (set.add(top * prime)) {
minHead.offer(top * prime);
}
}
}
return res;
}
}