Java堆与优先级队列示例详解

 更新时间:2026年07月17日 10:16:33   作者:小贾growing~  
优先级队列是一种特殊的数据结构,它允许元素按照优先级进行出队操作,优先级高的元素先出队,这篇文章主要介绍了Java堆与优先级队列的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

在 Java 开发者的语境里,提起“堆(Heap)”,我们第一反应可能是 JVM 内存区域。但那是内存管理概念,我们要聊的是数据结构里的堆——一个为了“极速取最值”而生的天才设计。

一、 为什么需要“堆”?

如果我们要在 100 万个数里实时找到最小的那个,Arrays.sort() 慢得像蜗牛,因为它做了大量“维持整体有序”的无用功。

  • 核心痛点:堆不追求全员有序,它只关注最值
  • 定义:一棵完全二叉树(要了解完全二叉树的结构,之所以使用完全二叉树就是因为它的特点,可以根据父节点的下标找到对应左右孩子节点,也可以根据孩子节点找到父节点下标),满足父节点永远优于子节点(大顶堆 ≥\ge,小顶堆 ≤\le)。
  • 真面目:逻辑上是树,物理上是数组。

二、 底层映射:如何在数组里“爬树”?

堆之所以快,是因为它抛弃了复杂的 Node 指针,直接利用完全二叉树的连续性,把数据平铺在数组里。

这是手写堆的灵魂公式(假设当前索引为 iii):

  • 找爸爸(Parent):parent(i)=(i−1)/2
  • 左儿子(Left)left(i)=2i+1
  • 右儿子(Right):right(i)=2i+2

这种设计极大地节省了内存,且对 CPU 缓存非常友好。

三、 手写核心算法:堆的“自我修复”

堆的秩序全靠这两个动作维持。在 Java 中,建议用迭代(while 循环)代替递归,性能更稳。

1. Sift Up (向上调整):新人的晋升

新元素插在末尾,不断和父亲比大小,比父亲强就“上位”。

private void siftUp(int k) {
    while (k > 0) {
        int parent = (k - 1) / 2;
        if (heap[k] <= heap[parent]) break; // 大顶堆逻辑
        swap(k, parent);
        k = parent;
    }
}

2. Sift Down (向下调整):老大的退位

弹出堆顶后,把末尾小弟顶上来,然后它要在两个儿子里挑个最强的交换,直到各就各位。

private void siftDown(int k) {
    while (2 * k + 1 < size) {
        int j = 2 * k + 1; 
        if (j + 1 < size && heap[j + 1] > heap[j]) j++; // 挑出更强的儿子
        if (heap[k] >= heap[j]) break;
        swap(k, j);
        k = j;
    }
}

复杂度分析:调整过程只跟树高相关,插入和删除均为 O(logn)。

四、 落地 Java:PriorityQueue 源码避坑

Java 的 PriorityQueue 默认是小顶堆。如果你想变大顶堆,得这么写:

PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());

关键细节:

  • 扩容:小容量时翻倍+2,大容量(>64)时增加 50%。
  • 遍历陷阱:用 for-each 遍历 PriorityQueue 结果是无序的(因为它按数组下标打印),想要有序必须不停调用 poll()
  • 线程安全:它是非线程安全的,高并发场景请出门左转找 PriorityBlockingQueue

五、 实战:用“末位淘汰制”解决 Top-K

求前 KKK 个最大的数,反而要用小顶堆

逻辑

  1. 用前 KKK 个数建个小顶堆,堆顶是当前的“守门员”(最小值)。
  2. 后面的数只要比“守门员”大,就踢走堆顶,自己进去调整。
  3. 优势:空间复杂度仅O(K),时间  O(Nlog⁡K)。在处理海量日志、实时热搜时,这是唯一可行的方案。

六、 总结

总的来说,堆这种结构在 Java 开发中就像是一个“高效的守门员”:它不追求全体有序,只专注于以 O(log⁡n)O(\log n)O(logn) 的极低代价压榨出最值。当你面对海量数据监控、定时任务调度(如 Timer 类的底层)或是像 Top-K 这种经典的性能瓶颈场景时,PriorityQueue 往往是比 Arrays.sort() 更聪明的选择。但在使用时也要留个心眼,记住它那“父子有序、兄弟乱序”的脾气,以及非线程安全的局限性。如果需要全序排列,隔壁的 TreeMap 或许更适合你,但论起处理流式数据的快准狠,堆依然是数据结构里的不二之选。

面试如果被问到“堆”,别只说它是完全二叉树,试着聊聊 siftDown 里的局部交换优化,或者 Top-K 里的逆向思维,这可能是拉开差距的地方。

到此这篇关于Java堆与优先级队列的文章就介绍到这了,更多相关Java堆与优先级队列内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring中的@Configuration详解

    Spring中的@Configuration详解

    这篇文章主要介绍了Spring中的@Configuration详解,Spring 提供了丰富的特性和功能,包括依赖注入、面向切面编程、事务管理、数据访问、Web应用程序开发等,需要的朋友可以参考下
    2023-08-08
  • JSON反序列化Long变Integer或Double的问题及解决

    JSON反序列化Long变Integer或Double的问题及解决

    这篇文章主要介绍了JSON反序列化Long变Integer或Double的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • 浅谈Java中父类与子类的加载顺序详解

    浅谈Java中父类与子类的加载顺序详解

    本篇文章是对Java中父类与子类的加载顺序进行了详细的分析介绍,需要的朋友参考下
    2013-06-06
  • Java调用IK分词器进行分词方式,封装工具类

    Java调用IK分词器进行分词方式,封装工具类

    这篇文章主要介绍了Java调用IK分词器进行分词方式,封装工具类,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08
  • JAVA生产者消费者(线程同步)代码学习示例

    JAVA生产者消费者(线程同步)代码学习示例

    这篇文章主要介绍了JAVA线程同步的代码学习示例,大家参考使用吧
    2013-11-11
  • Java动态代理简单介绍

    Java动态代理简单介绍

    动态代理指的是,代理类和目标类的关系在程序运行的时候确定的,客户通过代理类来调用目标对象的方法,是在程序运行时根据需要动态的创建目标类的代理对象。本文将通过案例详细讲解一下Java动态代理的原理及实现,需要的可以参考一下
    2022-08-08
  • Spring框架构造注入type属性实例详解

    Spring框架构造注入type属性实例详解

    这篇文章主要介绍了Spring框架构造注入type属性实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • SpringBoot实现接口幂等性的4种方案

    SpringBoot实现接口幂等性的4种方案

    这篇文章主要介绍了SpringBoot实现接口幂等性的4种方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • java几种排序算法的实现及简单分析

    java几种排序算法的实现及简单分析

    这篇文章主要介绍了java几种排序算法的实现及简单分析,实例分析了插入排序、希尔排序、选择排序等常用排序算法,并分析了各个算法的优劣,需要的朋友可以参考下
    2015-05-05
  • Java8 Lamda方法引用和构造引用原理

    Java8 Lamda方法引用和构造引用原理

    这篇文章主要介绍了Java8 Lamda方法引用和构造引用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04

最新评论