【堆的应用——topK问题】

本文详细介绍了如何使用优先级队列解决LeetCode中的三个TopK问题,包括求最小的k个数、前k个高频元素以及查找和最小的k对数字。通过构建最大堆或最小堆,实现了在nlogK的时间复杂度内找到所需结果,优于排序法的时间复杂度。
摘要由CSDN通过智能技术生成

目录

 topK问题是什么

1.力扣面试题17.14号问题——求最小的k个数

2.LeetCode 第347问题——前 K 个高频元素

3.力扣第373号问题——查找和最小的 K 对数字


 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 的规律,我们就构建最小堆

解题步骤:

  1. 将数组中出现的元素以及元素出现次数存储到Map中
  2. 扫描这个Map集合,构建最小堆,不断将大于堆顶元素的数入队,扫描完毕后最小堆就存储了前k个频次最高的元素
  3. 将最小堆依次出队即得到答案

代码实现:


//前 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 是可能比数组长度大的,那么我们遍历的终止条件是什么呢?

  1. 当 k > num.length 时,将数组遍历完毕
  2. 当 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问题就告一段落啦,可以看到这三个题中涉及到数对的,我们都构建了类来存储数据,今后做题过程中也可以尝试。尤其是最后一题是难度比较大的,但是没关系,我们要多花点时间攻克它!

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值