力扣Hot100系列16(Java)——[堆]总结()

文章目录


前言

本文记录力扣Hot100里面关于堆的三道题,包括常见解法和一些关键步骤理解,也有例子便于大家理解


一、数组中的第K个大的元素

1.题目

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素 ,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入: [3,2,1,5,6,4], k = 2

输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6], k = 4

输出: 4

2.代码

java 复制代码
class Solution {
    public int findKthLargest(int[] nums, int k) {

        Queue<Integer> queue = new PriorityQueue<>();
        // new PriorityQueue<>((a, b) -> (b - a)); 如果这样写就是大顶堆
        
        for (int i = 0; i < nums.length; i++) {
            queue.offer(nums[i]);
            if(queue.size() > k){
                queue.poll();
            }
        }
        
        return queue.poll();
    }
}

Queue<Integer> queue = new PriorityQueue<>();

Java的PriorityQueue默认是小顶堆(堆顶元素是整个堆中最小的元素

3. 例子

输入数组:nums = [3,2,1,5,6,4]

要找的目标:第2个最大的元素(预期结果是5)

核心逻辑:用小顶堆维护当前最大的k个元素,超过k个就移除堆里最小的元素,最终堆顶就是第k大元素。

  1. 初始化 :创建一个空的小顶堆 queue
  2. 遍历第一个元素 3
    • 把3加入堆,堆内元素为 [3](小顶堆堆顶是3)。
    • 堆的大小是1,小于k=2,不做移除操作。
  3. 遍历第二个元素 2
    • 把2加入堆,堆会自动调整为小顶堆,堆内元素为 [2, 3](堆顶是2)。
    • 堆的大小是2,等于k=2,不做移除操作。
  4. 遍历第三个元素 1
    • 把1加入堆,堆内元素变为 [1, 3, 2](堆顶是1)。
    • 堆的大小是3,超过了k=2,需要移除堆顶(最小的元素1)。
    • 移除后堆内元素回到 [2, 3](堆顶是2)。
  5. 遍历第四个元素 5
    • 把5加入堆,堆内元素变为 [2, 3, 5](堆顶是2)。
    • 堆的大小是3,超过k=2,移除堆顶的2。
    • 移除后堆内元素变为 [3, 5](堆顶是3)。
  6. 遍历第五个元素 6
    • 把6加入堆,堆内元素变为 [3, 5, 6](堆顶是3)。
    • 堆的大小是3,超过k=2,移除堆顶的3。
    • 移除后堆内元素变为 [5, 6](堆顶是5)。
  7. 遍历第六个元素 4
    • 把4加入堆,堆会自动调整为小顶堆,堆内元素变为 [4, 6, 5](堆顶是4)。
    • 堆的大小是3,超过k=2,移除堆顶的4。
    • 移除后堆内元素回到 [5, 6](堆顶是5)。

最终结果

遍历完所有元素后,堆里只剩下数组中最大的2个元素:5和6(小顶堆的堆顶是5)。执行 return queue.poll(); 弹出堆顶元素5,这就是数组中第2个最大的元素,和预期结果一致

二、前k个高频元素

1.题目

给你一个整数数组 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]

2.代码

步骤:

  1. 初始化结果数组 :创建长度为k的数组res,用于存储最终的前k个高频元素。
  2. 统计元素频率
    • 初始化HashMap,以数组元素为key、元素出现次数为value;
    • 遍历数组,用getOrDefault方法更新每个元素的频率(不存在则默认0,存在则+1)。
  3. 初始化小顶堆
    • 创建存储Map.Entry(键值对)的优先队列,自定义比较器按value(频率)升序排序,形成小顶堆(频率最小的在堆顶)。
  4. 筛选前k个高频元素
    • 遍历HashMap的所有键值对,将每个键值对加入小顶堆;
    • 若堆的大小超过k,移除堆顶元素(当前堆中频率最小的元素),确保堆中始终只保留遍历过的元素里频率最高的k个。
  5. 提取结果并返回
    • 遍历k次,依次弹出堆顶的键值对,取出其中的key(元素值)存入结果数组;
    • 返回结果数组,数组内即为频率前k高的元素。
java 复制代码
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // 前k个高频元素,小顶堆
        int[] res = new int[k];
        
        // 统计每个元素的"频率",存储到Map中,key是元素值,value是元素的"频率"
        Map<Integer,Integer> map = new HashMap<>();
        for(int num : nums){
            map.put(num, map.getOrDefault(num, 0) + 1);
        }

        // 使用小顶堆,堆中存储的是 Map的<key,value> 键值对
        // 并且排序规则是以value为比较, 即以元素的"频率"大小为比较
        Queue<Map.Entry<Integer,Integer>> queue = new PriorityQueue<>(
            (o1,o2)-> o1.getValue()-o2.getValue()  // 注意这样写是小顶堆,反过来就是大顶堆
        );
        for(Map.Entry<Integer,Integer> entry : map.entrySet()){
            queue.add(entry);
            if(queue.size() > k){
                queue.poll();
            }
        }
        
        // 将堆内剩余k个元素结果放到数组中返回。剩余k个元素即"频率"最高的k个元素
        for(int i = 0; i < k; i++){
            res[i] = queue.poll().getKey(); // 由于堆中存的是键值对,所以这里需要getKey
        }
        return res;
    }
}

3.理解

1.PriorityQueue的排序规则

java 复制代码
        Queue<Map.Entry<Integer,Integer>> queue = new PriorityQueue<>(
            (o1,o2)-> o1.getValue()-o2.getValue()  // 注意这样写是小顶堆,反过来就是大顶堆
        );

场景1:存储基本类型(Integer、Double 等)

java 复制代码
// 这行代码:默认小根堆
Queue<Integer> queue = new PriorityQueue<>();
queue.add(3);
queue.add(1);
queue.add(2);
System.out.println(queue.peek()); // 输出 1(堆顶是最小元素)
  • PriorityQueue 对Integer等基本类型的包装类,默认按照自然顺序升序排列,所以堆顶是最小值,也就是小根堆。
  • 等价于手动写比较器:new PriorityQueue<>((a, b) -> a - b)(a - b 结果为正则a排在b后面,最小的在堆顶)。

场景2:存储自定义类型(如 Map.Entry)

java 复制代码
// 自定义小根堆(按value升序)
Queue<Map.Entry<Integer,Integer>> queue = new PriorityQueue<>(
    (o1,o2)-> o1.getValue()-o2.getValue()
);
  • 因为Map.Entry不是基本类型,没有默认的"自然顺序",所以必须手动指定比较器。
  • 这里的o1.getValue()-o2.getValue(),本质和a - b是同一个逻辑:按value升序排列,value最小的在堆顶,也就是按value排序的小根堆。

2.offer方法和add方法的区别

add()和offer()的核心功能是一样 的:都是向队列(堆)中添加元素 。两者的唯一区别在于添加失败时的处理方式------一个抛异常,一个返回布尔值。

1. 方法定义与失败处理(Java 官方规范)

方法 成功返回值 失败场景(队列满了) 失败处理方式
add(E e) true 队列容量达到上限 抛出 IllegalStateException 异常
offer(E e) true/false 队列容量达到上限 返回 false(不抛异常,静默失败)

2. 结合你的使用场景(PriorityQueue)

你在找第k大元素、数据流中位数等代码中用到的 PriorityQueue无界队列(理论上没有容量上限,只要内存足够就能一直加元素),所以:

  • 调用 add():永远返回 true,永远不会触发"队列满"的失败场景,也就永远不会抛异常;
  • 调用 offer():永远返回 true,同样不会触发失败场景。

结论 :在 PriorityQueue 中,add()offer() 完全等价,你可以随便换用,比如把代码里的 queue.offer(nums[i]) 改成 queue.add(nums[i]),运行结果不会有任何变化。

3. 什么时候能看出区别?(有界队列示例)

只有在有界队列 (比如 ArrayBlockingQueue,创建时指定固定容量)中,才能体现两者的差异:

java 复制代码
// 创建一个容量为2的有界队列
Queue<Integer> queue = new ArrayBlockingQueue<>(2);
queue.add(1); // 成功,返回true
queue.offer(2); // 成功,返回true

// 尝试添加第三个元素:
queue.add(3); // 失败,直接抛出 IllegalStateException 异常
boolean result = queue.offer(3); // 失败,返回false,不抛异常

总结

  1. 核心区别:添加失败时,add() 抛异常,offer() 返回 false
  2. 无界队列 (如 PriorityQueue)中,两者完全等价
  3. 有界队列中offer() 更适合优雅处理"队列满"的情况。

4. 例子

  • 输入数组:nums = [1,1,1,2,2,3]
  • 目标:找前 k=2 个高频元素(预期结果:1、2)

步骤1:初始化结果数组

创建长度为2的数组 res,此时 res = [0, 0](初始值)。

步骤2:统计元素频率

遍历数组 [1,1,1,2,2,3],用HashMap统计每个元素的出现次数:

  • 遍历1:map中1的频率从0→1
  • 遍历1:map中1的频率从1→2
  • 遍历1:map中1的频率从2→3
  • 遍历2:map中2的频率从0→1
  • 遍历2:map中2的频率从1→2
  • 遍历3:map中3的频率从0→1
    最终map内容:{1=3, 2=2, 3=1}(1出现3次,2出现2次,3出现1次)。

步骤3:初始化小顶堆

创建存储Map.Entry的优先队列,比较规则为"按value(频率)升序",此时堆为空。

步骤4:筛选前k个高频元素

遍历map的3个键值对,逐个加入堆并维护堆大小不超过2:

  1. 处理键值对 (1,3)
    • 加入堆,堆内元素:[(1,3)],堆大小1(≤2),不移除元素;
  2. 处理键值对 (2,2)
    • 加入堆,堆自动调整为小顶堆(按频率升序),堆内元素:[(2,2), (1,3)](堆顶是频率2的2),堆大小2(=2),不移除元素;
  3. 处理键值对 (3,1)
    • 加入堆,堆内元素:[(3,1), (1,3), (2,2)],堆大小3(>2);
    • 移除堆顶元素(频率最小的(3,1)),堆内剩余:[(2,2), (1,3)]

步骤5:提取结果并返回

遍历2次,弹出堆顶元素并提取key存入res:

  1. 第一次弹出堆顶 (2,2),提取key=2,res[0] = 2
  2. 第二次弹出堆顶 (1,3),提取key=1,res[1] = 1
    最终res = [2, 1],返回该数组(顺序不影响,只要是前2个高频元素即可)。

三、数据流中的中位数

1.题目

中位数是有序整数列表中的中间值 。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 arr = [2,3,4] 的中位数是 3 。
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
    实现 MedianFinder 类:
  • MedianFinder() 初始化 MedianFinder 对象。
  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10(-5) 以内的答案将被接受。

示例 1:

输入

"MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"

\[\], \[1\], \[2\], \[\], \[3\], \[\]

输出

null, null, null, 1.5, null, 2.0

解释

MedianFinder medianFinder = new MedianFinder();

medianFinder.addNum(1); // arr = [1]

medianFinder.addNum(2); // arr = [1, 2]

medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)

medianFinder.addNum(3); // arr[1, 2, 3]

medianFinder.findMedian(); // return 2.0

2.代码

1. 初始化(构造方法)

  • 定义两个优先队列:queMin 为大顶堆(存储较小一半元素,堆顶是该部分最大值),queMax 为小顶堆(存储较大一半元素,堆顶是该部分最小值)。

2. 添加元素(addNum 方法)

  • 判定归属:若数字≤queMin堆顶(或queMin为空),放入queMin;否则放入queMax
  • 平衡堆大小:
    • queMinqueMax多超过1个,将queMin堆顶移到queMax
    • queMax大小超过queMin,将queMax堆顶移到queMin
    • 最终保证queMin大小 = queMax大小 或 queMin大小 = queMax大小+1。

3. 查找中位数(findMedian 方法)

  • 若元素总数为奇数(queMin更大),中位数是queMin堆顶;
  • 若元素总数为偶数(两堆大小相等),中位数是两堆顶数值的平均值。
java 复制代码
class MedianFinder {
    // 大顶堆:存储较小的一半元素,堆顶是这部分的最大值
    PriorityQueue<Integer> queMin;
    // 小顶堆:存储较大的一半元素,堆顶是这部分的最小值
    PriorityQueue<Integer> queMax;

    // 初始化堆:queMin大顶堆,queMax小顶堆
    public MedianFinder() {
        queMin = new PriorityQueue<Integer>((a, b) -> (b - a));
        queMax = new PriorityQueue<Integer>((a, b) -> (a - b));
    }
    
    // 添加元素,维持堆平衡:queMin.size() = queMax.size() 或 queMin.size() = queMax.size()+1
    public void addNum(int num) {
        // 元素归入较小半区(queMin)
        if (queMin.isEmpty() || num <= queMin.peek()) {
            queMin.offer(num);
            // 平衡:queMin超出queMax+1时,移堆顶到queMax
            if (queMax.size() + 1 < queMin.size()) {
                queMax.offer(queMin.poll());
            }
        } else {
            // 元素归入较大半区(queMax)
            queMax.offer(num);
            // 平衡:queMax超过queMin时,移堆顶到queMin
            if (queMax.size() > queMin.size()) {
                queMin.offer(queMax.poll());
            }
        }
    }
    
    // 获取中位数:奇数取queMin堆顶,偶数取两堆顶平均值
    public double findMedian() {
        if (queMin.size() > queMax.size()) {
            return queMin.peek();
        }
        return (queMin.peek() + queMax.peek()) / 2.0;
    }
}

3. 例子

以5 → 2 → 7 → 1 → 9为例:

  • 数据流添加顺序:5 → 2 → 7 → 1 → 9
  • 规则:queMin(大顶堆,存较小半区)、queMax(小顶堆,存较大半区)
  • 保证 queMin.size() = queMax.size()queMin.size() = queMax.size()+1

初始化
queMin(大顶堆)为空,queMax(小顶堆)为空。

步骤1:添加元素 5

  1. 判定归属:queMin 为空,将5放入 queMin
  2. 平衡堆大小:queMin.size()=1queMax.size()=0(符合规则),无需调整;
  3. 当前堆状态:queMin=[5](堆顶5)、queMax=[]
  4. 查中位数:总数奇数,中位数=5。

步骤2:添加元素 2

  1. 判定归属:2 ≤ queMin 堆顶5,放入 queMin
  2. 平衡堆大小:queMin.size()=2queMax.size()=0queMinqueMax 多2个,超出规则),将 queMin 堆顶5移到 queMax
  3. 当前堆状态:queMin=[2](堆顶2)、queMax=[5](堆顶5);
  4. 查中位数:总数偶数,中位数=(2+5)/2=3.5。

步骤3:添加元素 7

  1. 判定归属:7 > queMin 堆顶2,放入 queMax
  2. 平衡堆大小:queMax.size()=2 > queMin.size()=1,将 queMax 堆顶5移到 queMin
  3. 当前堆状态:queMin=[5,2](大顶堆,堆顶5)、queMax=[7](堆顶7);
  4. 查中位数:总数奇数,中位数=5。

步骤4:添加元素 1

  1. 判定归属:1 ≤ queMin 堆顶5,放入 queMin
  2. 平衡堆大小:queMin.size()=3queMax.size()=1queMinqueMax 多2个),将 queMin 堆顶5移到 queMax
  3. 当前堆状态:queMin=[2,1](堆顶2)、queMax=[5,7](堆顶5);
  4. 查中位数:总数偶数,中位数=(2+5)/2=3.5。

步骤5:添加元素 9

  1. 判定归属:9 > queMin 堆顶2,放入 queMax
  2. 平衡堆大小:queMax.size()=3 > queMin.size()=2,将 queMax 堆顶5移到 queMin
  3. 当前堆状态:queMin=[5,1,2](大顶堆,堆顶5)、queMax=[7,9](堆顶7);
  4. 查中位数:总数奇数,中位数=5。

如果本篇文章对您有帮助,可以点赞,收藏或评论哦!!!关注主包不迷路,让我们一起向前进步吧!!

相关推荐
3 小时前
java关于内部类
java·开发语言
好好沉淀3 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
gusijin3 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder3 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
只是懒得想了3 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
吨~吨~吨~3 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟3 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
短剑重铸之日3 小时前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式
码农水水3 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
summer_du3 小时前
IDEA插件下载缓慢,如何解决?
java·ide·intellij-idea