目录
topK问题是什么
当我们看到最小或者最大的k的元素什么的,都是优先级队列的应用。使用时遵循取大用小,取小用大。也就是说,找最小的k个数,就构造最大堆;找最大的k个数,就构造最小堆。它的核心思想就是“打擂”的过程,不断将更大或者更小的数放入堆中。
我们来看几个例题就能熟练掌握它的用法了:
1.力扣面试题17.14号问题——求最小的k个数
面试题 17.14. 最小K个数https://leetcode-cn.com/problems/smallest-k-lcci/
题目描述:设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。
示例:
输入: arr = [1,3,5,7,2,4,6,8], k = 4 输出: [1,2,3,4]
题目分析:
以数组 arr = [1,3,5,7,2,4,6,8]为例,找前 4 个最小的数,就构造一个4个元素的最大堆,然后扫描这个数组,小于堆顶元素的数入堆,大于或等于的跳过。当把数组完全扫描完毕之后,最小堆中存放了最小的四个元素
扫描完整个数组,发现当前堆存储的就是我们要找的四个数,每一个比堆顶元素小的值都将堆顶出队,将该元素入堆,比堆顶值大的都不入堆,就是一个将堆不断变小的过程。
如果是找前k个最大值,就构造k个元素的最小堆,也就是说堆顶存储的是最小值,若数组元素值大于堆顶值,就将堆顶元素出队,将该元素入队。如果小于堆顶值,就是说小于堆中所有元素,也就不用入队了。
代码实现:
public int[] smallestK(int[] arr, int k) {
if (arr.length == 0 || k == 0){
return new int[0];
}
//构造一个最大堆,JDK默认的是最小堆,使用比较器改造
Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
//扫描数组
for (int i : arr){
if (queue.size() < k){
queue.offer(i);
}else {
//判断当前元素和堆顶元素的大小关系
int peek = queue.peek();
if(peek < i){
//比堆顶元素大,不入队
continue;
}else {
queue.poll();
queue.offer(i);
}
}
}
int[] ret = new int[k];
for (int i = 0; i < k; i++) {
ret[i] = queue.poll();
}
return ret;
}
若这个题我们使用排序法,例如最优的快排,那时间复杂度是 nlogN ,而我们使用的堆方法的时间复杂度是 nlogK ,其中 k 就是堆中的数。因为k远小于n,所以 nlogK 是远小于 nlogN 的。
2.LeetCode 第347问题——前 K 个高频元素
力扣https://leetcode-cn.com/problems/top-k-frequent-elements/题目描述:给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
示例:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
题目分析:这个题目的大小关系不是元素值的大小,而是元素出现次数的大小,也就是说,元素出现次数越多,“值”就越大,按照 topK 的规律,我们就构建最小堆。
解题步骤:
- 将数组中出现的元素以及元素出现次数存储到Map中
- 扫描这个Map集合,构建最小堆,不断将大于堆顶元素的数入队,扫描完毕后最小堆就存储了前k个频次最高的元素
- 将最小堆依次出队即得到答案
代码实现:
//前 K 个高频元素
public class Num347{
//出现次数越高,值越“大”、构建最小堆
//类Freq存储每个不重复的元素以及出现次数
class Freq implements Comparable<Freq>{
//元素值
private int key;
//元素出现次数
int times;
public Freq(int key,int times){
this.key = key;
this.times = times;
}
@Override
public int compareTo(Freq o) {
return this.times - o.times;
}
}
public int[] topKFrequent(int[] nums, int k) {
int[] ret = new int[k];
//先扫描原nums数组,将每个不重复的元素和出现次数存储到Map中
Map<Integer,Integer> map = new HashMap<>();
for (int i:nums){
// map.put(i,map.getOrDefault(i,0) +1);
//此句和下面等价
//i没有存储过
if(!map.containsKey(i)){
map.put(i,1);
}else {
//此时i已经在map中了
map.put(i,map.get(i) + 1);
}
}
//构建最小堆
Queue<Freq> queue = new PriorityQueue<>();
for (Map.Entry<Integer,Integer> entry: map.entrySet()) {
if(queue.size()<k){
//直接入队
queue.offer(new Freq(entry.getKey(),entry.getValue()));
}else {
//队顶元素
Freq a = queue.peek();
if(a.times > entry.getValue()){
//不入队
continue;
}else {
queue.poll();
queue.offer(new Freq(entry.getKey(),entry.getValue()));
}
}
}
for (int i=0; i <k;i++){
ret[i] = queue.poll().key;
}
return ret;
}
}
3.力扣第373号问题——查找和最小的 K 对数字
力扣https://leetcode-cn.com/problems/find-k-pairs-with-smallest-sums/题目描述:给定两个以 升序排列 的整数数组 nums1 和 nums2 , 以及一个整数 k 。定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 。请找到和最小的 k 个数对 (u1,v1), (u2,v2) ... (uk,vk) 。
示例:
输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:
[1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]
提示:
- 1 <= nums1.length, nums2.length <= 105
- -109 <= nums1[i], nums2[i] <= 109
- nums1 和 nums2 均为升序排列
- 1 <= k <= 1000
题目分析:
做到第三个题,我们可以熟练的运用 topK 规则了,求和最小的 k 对数,那就需要构建最大堆,两个数组和越小,“值”就越小。和上一题一样,构建类来存储数对。
根据提示可以看到 k 是可能比数组长度大的,那么我们遍历的终止条件是什么呢?
- 当 k > num.length 时,将数组遍历完毕
- 当 k < num.length 时,最多只取到数组的前 k 个元素
综上两种情况,可得遍历取数组长度和 k 二者的最小值
代码实现:
private class Pair{
//第一个数组
int u;
//第二个数组
int v;
public Pair(int u,int v){
this.u = u;
this.v = v;
}
}
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
//1.扫描两个数组,u来自于第一个数组,v来自第二位数组
//构建最大堆,u+v的值越大,元素值就越大
Queue<Pair> queue = new PriorityQueue<>(new Comparator<Pair>() {
@Override
public int compare(Pair o1, Pair o2) {
return (o2.u + o2.v) - (o1.u +o1.v);
}
});
//遍历数组
for (int i = 0; i < Math.min(nums1.length, k); i++) {
for (int j = 0; j < Math.min(nums2.length, k); j++) {
if(queue.size() < k){
queue.offer(new Pair(nums1[i],nums2[j]));
}else {
int add = nums1[i] + nums2[j];
Pair pair = queue.peek();
if (add > (pair.u + pair.v)){
continue;
}else{
queue.poll();
queue.offer(new Pair(nums1[i],nums2[j]));
}
}
}
}
//此时优先级队列中就存储了和最小的前k个pair对象
List<List<Integer>> ret = new ArrayList<>();
//当 k > 数组长度时,总共只有num.length个数对,不可能出队三次
for (int i = 0;i < k && (!queue.isEmpty()); i++){
List<Integer> temp = new ArrayList<>();
Pair pair = queue.poll();
temp.add(pair.u);
temp.add(pair.v);
ret.add(temp);
}
return ret;
}
到此topK问题就告一段落啦,可以看到这三个题中涉及到数对的,我们都构建了类来存储数据,今后做题过程中也可以尝试。尤其是最后一题是难度比较大的,但是没关系,我们要多花点时间攻克它!