hot100:347 前k个高频元素
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
示例 3:
输入: nums = [1,2,1,2,1,2,3,1,3,2], k = 2
输出: [1,2]
提示:
1 <= nums.length <= 105k的取值范围是[1, 数组中不相同的元素的个数]- 题目数据保证答案唯一,换句话说,数组中前
k个高频元素的集合是唯一的 ``
一、自定义比较器的概念
在 Java 中,比较器(Comparator) 是一个接口,用于自定义两个对象之间的比较规则,尤其适用于 排序或优先队列 等需要比较的场景。
Java 标准库中:
public interface Comparator<T> {
int compare(T o1, T o2);
}
-
返回值约定:
compare(a, b) < 0→ a 在 b 之前(a “小于” b)compare(a, b) = 0→ a 和 b 相等compare(a, b) > 0→ a 在 b 之后(a “大于” b)
简单说,就是告诉排序算法:“按照我定义的规则,这两个元素谁大谁小”。
二、自定义比较器的常用写法
1. 使用匿名内部类
Comparator<Integer> cmp = new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a - b; // 升序
}
};
2. 使用 Lambda 表达式(Java 8+)
Comparator<Integer> cmp = (a, b) -> a - b; // 升序
Comparator<Integer> cmp2 = (a, b) -> b - a; // 降序
在你的
PriorityQueue中,你用的是 lambda:(a, b) -> freqMap.get(a) - freqMap.get(b)这表示“按数字在
freqMap中的频率升序排序”。
3. 使用方法引用(Method Reference)
如果有类提供了比较方法,可以直接引用:
List<String> list = Arrays.asList("apple", "banana", "pear");
list.sort(Comparator.comparing(String::length)); // 按长度升序
三、优先队列和自定义比较器
在 Java 中:
PriorityQueue<Integer> pq = new PriorityQueue<>(comparator);
-
默认情况下,如果不提供 comparator → 小顶堆(即
compare(a,b)=a-b)。 -
自定义 comparator 可以:
- 小顶堆:按某种自定义规则保留最小值在堆顶。
- 大顶堆:按某种自定义规则保留最大值在堆顶。
在你的例子中:
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> freqMap.get(a) - freqMap.get(b));
- 频率小的排在堆顶
- 当堆容量超过 k 时,
pq.poll()会移除当前最小频率的元素,从而留下频率最高的 k 个元素。
四、自定义比较器的原理
-
优先队列内部实现
PriorityQueue内部是基于 堆(Heap) 实现的。- 比较器决定堆的结构。
- 当你调用
offer()时,堆会自动根据比较器调整位置(上浮或下沉)。
-
lambda 比较器原理
(a, b) -> freqMap.get(a) - freqMap.get(b)
-
实际上就是
compare(a, b):- 如果
freqMap.get(a) < freqMap.get(b)→ 返回负 →a上浮或排在前面 - 如果
freqMap.get(a) > freqMap.get(b)→ 返回正 →a下沉或排在后面
- 如果
⚠️ 注意整数溢出:
return freqMap.get(a) - freqMap.get(b);
当频率非常大时可能溢出,推荐使用:
return Integer.compare(freqMap.get(a), freqMap.get(b));
五、自定义比较器的常见运用场景
-
排序集合:
List<Person> list = ...; list.sort((a, b) -> b.age - a.age); // 按年龄降序 -
PriorityQueue 堆:
- Top K 问题(你的例子)
- 最小路径、最小生成树等图算法
PriorityQueue<Node> pq = new PriorityQueue<>((n1, n2) -> n1.weight - n2.weight); -
TreeMap / TreeSet(红黑树):
TreeMap<String, Integer> map = new TreeMap<>((a,b) -> b.compareTo(a)); // 倒序 -
自定义多条件排序:
list.sort((a,b) -> { int diff = a.age - b.age; return diff != 0 ? diff : a.name.compareTo(b.name); });
六、优化和高级技巧
- 使用
Comparator.comparing和thenComparing:
list.sort(Comparator.comparing(Person::getAge)
.thenComparing(Person::getName));
-
避免整数溢出:
Integer.compare(a, b); // 安全 -
倒序写法:
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder()); -
复杂对象比较:
pq = new PriorityQueue<>((p1, p2) -> { if (p1.score != p2.score) return p2.score - p1.score; // 分数降序 else return p1.time - p2.time; // 时间升序 }); -
Lambda 与匿名类的区别:
- Lambda 简洁,函数式接口可以直接写一行。
- 匿名类适合有多个方法逻辑或需要在内部保存状态的情况。
七、结合你的代码示例
PriorityQueue<Integer> pq = new PriorityQueue<>(
(a, b) -> freqMap.get(a) - freqMap.get(b)
);
-
作用:
- 用
freqMap中的值(频率)比较数字。 - 小频率在堆顶 → 便于弹出低频元素。
- 用
-
优势:
- 不需要全排序 O(n log n)
- 堆容量只维护 k 个 → 时间复杂度 O(n log k)
-
输出结果:
pq.poll()按从最小频率到最大频率弹出- 如果要得到从高到低 → 可以逆序填入结果数组或使用
Collections.reverse。
八、总结知识点
-
比较器作用:定义对象比较规则
-
使用方式:匿名类 / Lambda / 方法引用 / Comparator 工具方法
-
返回值规则:
- 负 → 前
- 0 → 相等
- 正 → 后
-
在堆中的作用:控制堆顶元素,动态维护顺序
-
多条件比较:可链式组合或手动判断
-
注意事项:
- 避免溢出
- 对复杂对象要明确比较逻辑
- TreeSet / TreeMap 要保证比较器一致性(不能违反
equals)
九、更多运用示例
// 1. 按字符串长度排序
List<String> words = Arrays.asList("apple","banana","pear");
words.sort(Comparator.comparingInt(String::length));
// 2. 按对象多条件排序
List<Student> students = ...
students.sort(Comparator.comparingInt(Student::getGrade)
.thenComparing(Student::getName));
// 3. 优先队列 Top K 问题
PriorityQueue<Integer> topK = new PriorityQueue<>(Comparator.comparingInt(x -> freqMap.get(x)));
for (int num : freqMap.keySet()) {
topK.offer(num);
if (topK.size() > k) topK.poll();
}
// 4. TreeMap 倒序
TreeMap<String,Integer> map = new TreeMap<>(Comparator.reverseOrder());