【数据结构】java对象的比较与TopK问题

文章详细介绍了Java中对象的比较方式,包括基于Comparable接口的比较和使用Comparator比较器的比较。接着,文章探讨了如何使用PriorityQueue解决TopK问题,例如找到最小的K个值和频率最高的K个值。此外,还提供了处理频率最高的K个单词的解决方案,强调了在不同情况下的比较策略和数据结构的选择。
摘要由CSDN通过智能技术生成

目录

🌟java对象的比较

✍前言

✍基于Comparble接口类的比较

​✍基于比较器比较 

🌟TopK问题

✍使用PriorityQueue创建大小堆,解决TOPK问题

 ✍最小的K个值

✍ 频率最高的K个值

✍ 频率最高的K个单词


🌟java对象的比较

✍前言

         java中我们最常熟知的就是基本数据类型的比较,引用数据类型:对于用户实现的自定义类型,一般默认情况下调用equal方法,比较引用变量的地址。但是equal只能按照相等进行比较,不能按照大于,小于的方式进行比较。

基于Comparble接口类的比较

        Comparable是JDK提供的泛型的比较接口类,java.lang.Comparable:一个类若实现了Comparable接口,就表示该类具备可比较的能力。源码实现如下:

public interface Comparable<E> {
// 返回值:
// < 0: 表示 this 指向的对象小于 o 指向的对象
// == 0: 表示 this 指向的对象等于 o 指向的对象
// > 0: 表示 this 指向的对象大于 o 指向的对象
int compareTo(E o);
}

        对用用户自定义类型,如果要想按照大小与方式进行比较时:在定义类时,实现Comparble接口即可,然后在类中重写compareTo方法。

案例:比如以下自定义的学生类对象:包含姓名和年龄两个属性,现在要求按照年龄比较,在优先级队列中存储年龄最小的两个学生对象。保存的是最大年龄还是最小年龄关键在于重写的compareTo方法的比较方式。

public class Student implements Comparable<Student>{
    String name;
    int age;
    public Student(){};
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    //重写compareTo方法
    @Override
    public int compareTo(Student o) {
        //return this.age - o.age;//保存的是年龄最大的
        return o.age- this.age;//保存的是年龄最小的        
    }
    public static void main(String[] args) {
        Student s1 = new Student("A",21);
        Student s2 = new Student("B",25);
        Student s3 = new Student("H",19);
        Student s4 = new Student("K",31);
        Student s5 = new Student("C",26);
        Student[] students = {s1,s2,s3,s4,s5};
        //--------------------Comparable接口方式---------------------------
        //System.out.println(s1.compareTo(s2));
        //比较s1与s2,s1-s2<0,表明s1指向的对象小于s2的对象
        //现在要获取年纪最小的两个学生
        Queue<Student> queue = new PriorityQueue<>();
        for (Student student: students) {
            queue.offer(student);
            if(queue.size() > 2){
                queue.poll();
            }
        }
        System.out.println(queue);        
    }

}

打印结果: 


 ✍基于比较器比较 

java.util.Compartor接口:实现了该类的接口,主要是专门负责给某些类进行大小比较的。

使用Compartor比较器对学生的年龄进行比较。(因为优先级队列的默认实现是最小堆,而取年龄最小的则要用最大堆,所以这里要改写比较的方式为o2.age-o1.age)。

//比较器方式
class StudentAgeSec implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o2.age - o1.age;//o2-o1则是保存的年龄最大的
        return o1.age - o2.age;//保存的是年龄最小的
    }
}
public class Student implements Comparable<Student>{
    String name;
    int age;
    public Student(){};
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    public static void main(String[] args) {
        Student s1 = new Student("A",21);
        Student s2 = new Student("B",25);
        Student s3 = new Student("H",19);
        Student s4 = new Student("K",31);
        Student s5 = new Student("C",26);
        Student[] students = {s1,s2,s3,s4,s5};
        //----------------------------------比较器方式---------------------
        //现在要获取年纪最小的两个学生
        Queue<Student> queue = new PriorityQueue<>(new StudentAgeSec());
        for (Student student: students) {
            queue.offer(student);
            if(queue.size() > 2){
                queue.poll();
            }
        }
        System.out.println(queue);
       
    }

}

比较结果:

  ✅总结:要使用JDK的优先级队列,元素必须具备可比较的能力(两种方法:该类实现了Comparable接口或者传入一个该类的比较器Compartor对象)。而且,如果一个类本身实现了Comaparable接口,同时又在对列中传入了比较器对象,则以比较器对象为准。
(1)Comparble是默认的内部比较方式,如果用户插入自定义类型对象时,该类对象必须要实现Comparble接口,并覆写compareTo方法;
(2)用户也可以选择使用比较器对象,如果用户插入自定义类型对象时,必须要提供一个比较器类,让该类实现Comparator接口并覆写compare方法。


🌟TopK问题

✍使用PriorityQueue创建大小堆,解决TOPK问题

        优先级队列中默认使用的是最小堆,要使用最大堆,则要进行改写。注意区别。而且对于TopK问题,一般规律都是:取小用大,取大用小(取较小的数字用大堆,取大数用小堆)

 ✍最小的K个值

要求:找出数组汇总最小的K个数。

(1)方法1:排序法:将数组中的所有元素排序,取出前k个即可

    public int[] smallestK(int[] arr, int k) {
        Arrays.sort(arr);
        int[] result = new int[k];
        for (int i = 0; i < arr.length; i++) {
            result[i] = arr[i];
        }
        return result;
    }

 (2)方法2:将数组中的元素都添加到最小堆里面,依次出堆k次。(因为最小堆的堆顶保存的一定是最小值)

    public int[] smallestK(int[] arr, int k) {
        Queue<Integer> queue = new PriorityQueue<>();
        for (int i : arr) {
            queue.offer(i);
        }
        int[] result = new int[k];
        for (int i: arr) {
            result[i] = arr[i];
        }
        return result;
    }

 (3)重点方法:设计一个时间复杂度小于n*logn的算法 、

         思路:建立一个只保存k个元素的最大堆(先要改写最小堆为最大堆),扫描整个数组。  Step1:若当前最大堆的元素个数小于k,直接入队;Step2:当最大堆的元素个数>=K时,如果当前元素最大堆的堆顶还大,则一定不是需要的元素。

        代码实现:        

    // 取小用大堆
    public int[] smallestK(int[] arr, int k) {
        // 1.改造JDK的最小堆为此时的最"大"堆
        Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        for (int i : arr) {
            queue.offer(i);
            if (queue.size() > k) {
                queue.poll();
            }
        }
        // 此时队列中保存了最小的k个数字
        int[] result = new int[k];
        for (int i = 0; i < k; i++) {
            result[i] = queue.poll();
        }
        return result;
    }

频率最高的K个值

        补充:解决此题我们需要知道Map哈希表的一些基本知识

  • Map是哈希表,保存的是一对元素 :key和value;
  • Map中添加元素:put方法:put(key,value);
  • Map的遍历方式:使用.entrySet()转为Set后遍历;
  • get(key),返回key对应的value值;

代码演示:

     public static void Map(){
        Map<String,String> map = new HashMap<>();
    }

    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("花和尚","鲁智深");
        map.put("智多星","吴用");
        map.put("豹子头","林冲");
        System.out.println(map);
        //map->set map的遍历方式
        Set<Map.Entry<String,String>> set = map.entrySet();
        for (Map.Entry<String,String> entry : set){
            System.out.println(entry.getKey()+"=" + entry.getValue());
        }
    }

打印结果: 

简单知道了上述Map的知识,我们就可以用来解此题了~

思路分析:

        要知道出现的频次最高的元素,那么我们需要一个东西既能保存该元素,又能保存该元素出现的次数。这个时候就用到了我们上面说到的哈希表Map。

  • 所以第一步:我们遍历数组,将每个元素和出现的次数都保存在Map中。
  • 第二步:我们现在需要获取出现频次最高的K个元素,那这和第一个题一样:我们使用优先级队列来获取前K个高频元素。高频元素则要使用最小堆:而最小堆的比较方式为出现频次的比较,所以要使用比较器进行比较方式的改写;然后遍历Map将前K个高频对象保存在优先级队列中;
  • 第三步:此时优先级队列中保存的就是前K个高频元素了,我们将它取出来存储在集合中即可。

代码实现:

    public int[] topKFrequent(int[] nums, int k) {
        //使用Map同时保存每个元素值和出现的频次
        //局部内部类
        class Freq{
            int key;//出现的元素
            int times;//出现的频率
            Freq(int key, int times){
                this.key = key;
                this.times = times;
            }
        }
        //1、遍历nums数组,将每个元素和出现的次数都保存在Map接口中
        Map<Integer,Integer> map = new HashMap<>();
        //此时数组的num就是map中的key:表示出现的元素
        for (int num: nums) {
            //这个方法是判断这个元素在不在map里面,在的话就+1,不在的话说明第一次出现就用0代替
            //map.put(num, map.getOrDefault(num,0)+1);
            if(map.containsKey(num)){
                int value = map.get(num); //map.get(key),返回key对应的value
                map.put(num, value+1);
            }else{
                map.put(num,1);
            }
        }
        //2、扫描Map接口,将出现频次最高的前k个Freq对象保存在优先级队列中
        Queue<Freq> queue = new PriorityQueue<>(new Comparator<Freq>() {
            @Override
            public int compare(Freq o1, Freq o2) {
                return o1.times-o2.times;
            }
        });
        //遍历Map
        for (Map.Entry<Integer,Integer> entry: map.entrySet()) {
            //获取Freq对象
            queue.offer(new Freq(entry.getKey(),entry.getValue()));
            if(queue.size() > k){
                queue.poll();
            }
        }
        //3、从队列中取出这k个FreMap汇总的key值就是最终的结果
        int[] result = new int[k];
        for (int i = 0; i < k; i++) {
            result[i] = queue.poll().key;
        }
        return result;
    }

频率最高的K个单词

要求:给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。

主要思路: 

        这道题要求出现频率最高的前K个单词,乍一看和上面那个题一样,不过可以发现这道题明显要求更多,要注意的细节也更多。

  • 首先按照出现的频率排序,而且输出的结果应该是按频次从高到低,是一个降序过程。那么什么怎么可以实现降序过程?->链表的头插;
  • 如果出现的频次相同,就按照字典序排,排序结果是升序。但是关键也在这里:我们刚才前一步使用了链表的头插,导致本来字典序结果是正常的升序,但是一颠倒又返回去了。(比如i 和 love,字典序排完之后应该是i在love之前,但是第一步之后,又变成了i在love之后了)两者之间出现了矛盾性:所以这里还需要再反转一下:更改一下字典序的排序方式,s2.compareTo(s1)即可。(🍀比较绕哈,要多理解理解~)

代码实现:

     public List<String> topKFrequent(String[] words, int k) {
        // 1.用Map来保存每个单词以及其出现的频次
        Map<String,Integer> wordsTable = new HashMap<>();
        for (String str : words) {
            if (wordsTable.containsKey(str)) {
                int times = wordsTable.get(str);
                wordsTable.put(str,times + 1);
            }else {
                wordsTable.put(str,1);
            }
        }
        // 此时wordsTable保存了每个不重复的单词以及其出现的频次
        // 取大用小
        Queue<String> queue = new PriorityQueue<>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                // 先按照出现频次来判定
                int o1Times = wordsTable.get(o1);
                int o2Times = wordsTable.get(o2);
                // 频次相同按照字典序排序
                // 由于最终使用头插法,此时的字典序和ASCII"反"着来
                return o1Times == o2Times ? o2.compareTo(o1) : o1Times - o2Times;
            }
        });
        // 2.扫描Map集合,保存出现频次最高的前k个单词
        for (Map.Entry<String,Integer> entry : wordsTable.entrySet()) {
            queue.offer(entry.getKey());
            if (queue.size() > k) {
                queue.poll();
            }
        }
        // 3.此时优先级队列中就保存了出现频次最高的前k个单词,进行输出保存
        LinkedList<String> result = new LinkedList<>();
        while (!queue.isEmpty()) {
            // 现在是小堆,出队时按照出现频次由低到高进行出队
            // 要实现从高到低的保存,使用头插法来保存元素~
            result.addFirst(queue.poll());
        }
        return result;
    }

        周三学习周五玩耍,我可以!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值