4.347:前K个高频元素(自定义比较器的使用)

55 阅读4分钟

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 <= 105
  • k 的取值范围是 [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 个元素。

四、自定义比较器的原理

  1. 优先队列内部实现

    • PriorityQueue 内部是基于 堆(Heap) 实现的。
    • 比较器决定堆的结构。
    • 当你调用 offer() 时,堆会自动根据比较器调整位置(上浮或下沉)。
  2. 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));

五、自定义比较器的常见运用场景

  1. 排序集合

    List<Person> list = ...;
    list.sort((a, b) -> b.age - a.age); // 按年龄降序
    
  2. PriorityQueue 堆

    • Top K 问题(你的例子)
    • 最小路径、最小生成树等图算法
    PriorityQueue<Node> pq = new PriorityQueue<>((n1, n2) -> n1.weight - n2.weight);
    
  3. TreeMap / TreeSet(红黑树):

    TreeMap<String, Integer> map = new TreeMap<>((a,b) -> b.compareTo(a)); // 倒序
    
  4. 自定义多条件排序

    list.sort((a,b) -> {
        int diff = a.age - b.age;
        return diff != 0 ? diff : a.name.compareTo(b.name);
    });
    

六、优化和高级技巧

  1. 使用 Comparator.comparingthenComparing
list.sort(Comparator.comparing(Person::getAge)
                    .thenComparing(Person::getName));
  1. 避免整数溢出

    Integer.compare(a, b); // 安全
    
  2. 倒序写法

    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
    
  3. 复杂对象比较

    pq = new PriorityQueue<>((p1, p2) -> {
        if (p1.score != p2.score)
            return p2.score - p1.score; // 分数降序
        else
            return p1.time - p2.time; // 时间升序
    });
    
  4. 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

八、总结知识点

  1. 比较器作用:定义对象比较规则

  2. 使用方式:匿名类 / Lambda / 方法引用 / Comparator 工具方法

  3. 返回值规则

    • 负 → 前
    • 0 → 相等
    • 正 → 后
  4. 在堆中的作用:控制堆顶元素,动态维护顺序

  5. 多条件比较:可链式组合或手动判断

  6. 注意事项

    • 避免溢出
    • 对复杂对象要明确比较逻辑
    • 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());