目录
✍使用PriorityQueue创建大小堆,解决TOPK问题
🌟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);
}
}
打印结果:
![](https://i-blog.csdnimg.cn/blog_migrate/8395c3cdb73f998c23c4073431cc29a2.png)
✍基于比较器比较
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;
}
周三学习周五玩耍,我可以!!!