【数据结构】优先级队列的实际应用之TopK问题

1.前置知识—对象大小的比较

整形、浮点型等数值型大小的比较,直接比较值的大小即可,而对象间大小的比较,有以下两种方式:

  • 实现Comparable接口,重写compareTo方法

一个类一旦实现了Comparable接口,就表明当前类具备了可比较大小的能力,根据compareTo的返回值确定对象间的大小。

public class Test1 {

    static class  Student implements Comparable<Student>{
        private String name;
        private int age;

        public Student(String name,int age){
            this.name =name;
            this.age =age;
        }
        @Override
        public int compareTo(Student o) {
            return o.age -this.age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

    public static void main(String[] args) {
        Student s1 =new Student("胡歌",30);
        Student s2 =new Student("江江",24);
        Student s3 =new Student("浩存",00);
        Student[] students =new Student[]{s1,s2,s3};
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}

执行结果:
在这里插入图片描述
着重注意compareTo方法的返回值:在这里插入图片描述
要知道java中 Arrays.sort()方法默认是按升序排列,此处在compareTo方法中执行策略是按年龄的降序排列。

  • 实现Comparator接口,重写compare方法

与Comparable相似,一个类一旦实现了Comparator接口,就表明当前类具备了可比较大小的能力,根据compare的返回值确定对象间的大小。

public class Test2 {

    static class  Student{
        private String name;
        private int age;

        public Student(String name,int age){
            this.name =name;
            this.age =age;
        }

        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    /**
     * 升序的比较器
     */
    static class StudentAsc implements Comparator<Student>{
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getAge()-o2.getAge();
        }
    }
    /**
     * 降序的比较器
     */
    static class StudentDesc implements Comparator<Student>{
        @Override
        public int compare(Student o1, Student o2) {
            return o2.getAge()-o1.getAge();
        }
    }

    public static void main(String[] args) {
        Student s1 =new Student("胡歌",30);
        Student s2 =new Student("江江",24);
        Student s3 =new Student("浩存",00);
        Student[] students =new Student[]{s1,s2,s3};
        Arrays.sort(students,new StudentAsc());
        System.out.println(Arrays.toString(students));
        Arrays.sort(students,new StudentDesc());
        System.out.println(Arrays.toString(students));
    }
}

执行结果:
在这里插入图片描述

从以上结果中看出,在排序过程中传入不同的比较策略就可以得到不同的排序结果,但是Comparator相比较Comparable来说更加灵活,无需修改类的代码,属于无侵入式编程。

2.自定义比较器实现优先级队列

自定义一个降序的比较器,创建优先级队列时传入比较器

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

public class Test3 {

    static class  Student{
        private String name;
        private int age;

        public Student(String name,int age){
            this.name =name;
            this.age =age;
        }

        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    /**
     * 升序的比较器
     */
    static class StudentAsc implements Comparator<Student>{
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getAge()-o2.getAge();
        }
    }

    public static void main(String[] args) {
        Student s1 =new Student("胡歌",30);
        Student s2 =new Student("江江",24);
        Student s3 =new Student("浩存",20);
      //1.比较器写法
        Queue<Student> queue =new PriorityQueue<>(new StudentDesc());
        //2.匿名内部类写法
        Queue<Student> queue1 =new PriorityQueue<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o2.age-o1.getAge();
            }
        });
        //3.lambda表达式
        Queue<Student> queue2 =new PriorityQueue<>(((o1, o2) -> o2.age- o1.age));
        queue.offer(s1);
        queue.offer(s2);
        queue.offer(s3);
        while (!queue.isEmpty()){
            System.out.println(queue.poll());
        }
    }
}

执行结果:
在这里插入图片描述

3.优先级队列的实际应用—TopK问题

最大堆性质:堆顶元素是最大值,你是要找小的,若扫描的元素大于堆顶元素,则大于堆中的所有值,这个值一定不是你要找的值。
经典TopK问题
1.数组中最小K个数

public class Lc1714_SmallestK {

    /**
     * 数组中前K个最小数
     * @param arr
     * @param k
     * @return
     */
    public int[] smallestK(int[] arr, int k) {
    //边界条件
        if (arr.length ==0 || k==0){
        return new int[0];
    }
        //构造一个基于最大堆的优先级队列
        Queue<Integer> queue =new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;
            }
        });
        //遍历数组,将数组中最大的k个数放入队列中
        for (int i : arr) {
            //队列中元素小于k,直接入队
            if (queue.size()<k){
                queue.offer(i);
            }else {
                //此时,队列已满,取出堆顶元素,判断当前元素和堆顶元素的大小
                 int peek = queue.peek();
                 if (peek<i){
                     //当前元素大于堆顶元素
                     continue;
                 }else {
                     //当前元素小于堆顶元素,则一定小于堆中所有的元素,堆顶元素出队,将当前元素入队
                     queue.poll();
                     queue.offer(i);
                 }
            }
        }
        //此时,堆中就保存了最小的k个数
        int[] ret =new int[k];
        for (int i = 0; i < k; i++) {
            ret[i] =queue.poll();
        }
        return ret;
    }
}

2.前 K 个高频元素

class Solution {
       public int[] topKFrequent(int[] nums, int k) {
        //定义一个map集合存储数组元素出现的次数
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            if (map.containsKey(num)) {
                map.put(num, map.get(num) + 1);
            } else {
                map.put(num, 1);
            }
        }
        //构造一个最小堆保存最大得K个元素
        PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return map.get(o1) - map.get(o2);
            }
        });
        //遍历map集合,保存最大的K个元素
        for (Integer key : map.keySet()) {
            //队列还未满时,直接将元素入队
            if (queue.size() < k) {
                queue.offer(key);
            } else if (map.get(key) > map.get(queue.peek())) {
                //将堆顶元素出队
                queue.poll();
                queue.offer(key);
            }
        }   
        //此时的堆就保存最大的K个元素
        int[] ret = new int[k];
        for (int i = 0; i < k; i++) {
            ret[i] = queue.poll();
        }
        return ret;
    }
}

3.查找和最小的 K 对数字

class Solution {
 public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        //构造一个大根堆
        Queue<List<Integer>> queue =new PriorityQueue<>(((o1, o2) -> (o2.get(0)+o2.get(1))-(o1.get(0)+o1.get(1))));
        int n1 = nums1.length;;
        int n2= nums2.length;
        for (int i = 0; i < n1; i++) {
            for (int j = 0; j < n2; j++) {
                int sum =nums1[i]+nums2[j];
                List<Integer> list =new ArrayList<>();
                //每次存入一组数对到集合
                list.add(nums1[i]);
                list.add(nums2[j]);
                if (queue.size()<k) {
                    //队列未满,直接将集合加入队列
                    queue.offer(list);
                }else {
                    //取出堆顶元素和
                    int s =queue.peek().get(0)+queue.peek().get(1);
                    if (s>sum){
                        //堆顶元素之和大于当前数组和
                        queue.poll();
                        queue.offer(list);
                    }else {
                        break;
                    }
                }
            }
        }
        //此时优先级队列中就存储了前k小个数对
     List<List<Integer>> ans =new ArrayList<>(queue);
        return ans;
    }
}

总结,以上三个题型是比较经典的TopK问题,TopK的其他问题解法与之类似,TopK问题的另一种解法就是排序,但在数量级比较大的情况下,构造一个最大/小堆来解决问题,时间复杂度在NlogN,效率就要高很多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值