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;
}
}
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;
}
}
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,效率就要高很多。