当前位置: 首页 > news >正文

优先级队列 与 堆

        在日常生活中,“优先级” 无处不在:手机玩游戏时来电会优先中断游戏,班主任排座位可能让成绩好的同学先选,外卖配送会优先处理距离近或订单金额高的单。这些场景背后,都隐藏着一种特殊的数据结构 ——优先级队列。而优先级队列的高效实现,离不开另一个核心结构 ——堆(Heap)

一、 什么是优先级队列?

        普通队列遵循 “先进先出”(FIFO)的规则,就像排队买咖啡,先到的人先拿到饮品。但优先级队列打破了这种 “公平性”,它的核心规则是:优先级高的元素先出队

优先级队列需要支持两个核心操作:

  • 插入新元素(offer):将元素加入队列并维护优先级顺序;
  • 取出优先级最高的元素(poll):移除并返回队列中优先级最高的元素;
  • 查看优先级最高的元素(peek):不删除,仅返回最高优先级元素。

二、 堆:优先级队列的 “底层引擎”

        Java 的 PriorityQueue 底层采用来实现,因为堆能高效支持优先级队列的核心操作(插入、删除均为 O(logN) 时间复杂度)。那堆到底是什么?

2.1 堆的定义与性质

堆是一种完全二叉树(按层序存储,除最后一层外每一层都满,最后一层从左到右连续),且满足以下 “父子大小关系”:

  • 小根堆:每个父节点的值 ≤ 其左右子节点的值(堆顶是最小值);
  • 大根堆:每个父节点的值 ≥ 其左右子节点的值(堆顶是最大值)。

2.2 堆的存储方式

        由于堆是完全二叉树,我们可以用一维数组高效存储(无需存储空节点,空间利用率 100%)。数组中节点的下标满足以下规律(假设节点下标为 i):

  • 父节点下标:(i - 1) / 2(整数除法,忽略小数);
  • 左孩子下标:2 * i + 1(若小于数组长度则存在左孩子);
  • 右孩子下标:2 * i + 2(若小于数组长度则存在右孩子)。

        比如小根堆 [10,15,25,56,30,70],下标 0 是根(10),左孩子是下标 1(15),右孩子是下标 2(25),完全符合上述规律。

三、 堆的核心操作实现

        堆的一切操作都围绕 “维护堆的性质” 展开,核心是两个调整动作:向下调整向上调整

3.1 向下调整:修复堆的 “基石”

        向下调整的前提是:当前节点的左右子树均已满足堆的性质,仅当前节点可能破坏堆结构。我们需要将当前节点 “下沉” 到合适位置,让堆恢复有序。

以小根堆为例,向下调整步骤:

  1. 标记当前节点(parent)和其左孩子(child = 2*parent + 1);
  2. 若右孩子存在且比左孩子小,更新 child 为右孩子(找到更小的孩子);
  3. 比较 parent 和 child
    • 若 parent ≤ child:堆已有序,调整结束;
    • 若 parent > child:交换两者,然后将 parent 指向 childchild 指向新的左孩子,重复步骤 2-3。

代码实现(小根堆)

//对array中以parent为根的子树进行向下调整(小根堆)
public static void shiftDown(int[] array, int size, int parent) {int child = 2 * parent + 1; //先找左孩子while (child < size) { //孩子存在才需要调整//1. 找左右孩子中更小的那个if (child + 1 < size && array[child + 1] < array[child]) {child++; //右孩子更小,更新child}//2. 比较父节点和最小孩子if (array[parent] <= array[child]) {break; //父节点更小,堆有序} else {//交换父节点和孩子int temp = array[parent];array[parent] = array[child];array[child] = temp;//继续向下调整(子树可能被破坏)parent = child;child = 2 * parent + 1;}}
}

时间复杂度:最坏情况从根调整到叶子,需遍历树的高度,即 O(logN)

3.2 堆的创建:从无序到有序

        如果给一个无序数组,如何将其建成堆?关键是从最后一个非叶子节点开始,依次向前做向下调整

        为什么从最后一个非叶子节点开始?因为叶子节点本身就是 “单个节点的堆”,无需调整;最后一个非叶子节点的下标是 (size - 2) / 2(推导:最后一个节点下标是 size-1,其父节点就是 (size-1 -1)/2 = (size-2)/2)。

代码实现(建小根堆)

public static void createHeap(int[] array) {int size = array.length;//从最后一个非叶子节点开始,向前遍历每个节点并调整for (int parent = (size - 2) / 2; parent >= 0; parent--) {shiftDown(array, size, parent);}
}

        时间复杂度:看似每个调整是 O(logN),但整体是 O(N)(数学推导略,核心是上层节点调整次数少,下层节点多但调整次数少,加权后趋近于 N)。

3.3 堆的插入与删除

3.3.1 插入:“上浮” 调整

        插入元素时,为了保持完全二叉树的结构,我们先将元素放到数组末尾,再通过 “向上调整” 让其回到合适位置(小根堆为例):

步骤:

  1. 将新元素插入数组末尾(size++);
  2. 标记新元素为 child,找到其父节点 parent = (child - 1) / 2
  3. 比较 child 和 parent
    • 若 child ≥ parent:堆有序,调整结束;
    • 若 child < parent:交换两者,child 指向 parentparent 指向新的父节点,重复步骤 3。

代码实现

public static void shiftUp(int[] array, int size, int child) {int parent = (child - 1) / 2;while (child > 0) { //孩子不是根节点才需要调整if (array[child] >= array[parent]) {break; //堆有序} else {//交换int temp = array[child];array[child] = array[parent];array[parent] = temp;//继续向上child = parent;parent = (child - 1) / 2;}}
}//插入元素
public static boolean offer(int[] array, int size, int value) {//实际场景需考虑扩容,这里简化array[size] = value;shiftUp(array, size + 1, size); //新元素下标是size,调整后size+1return true;
}
3.3.2 删除:仅删堆顶,“下沉” 调整

        堆的删除有个规定:只能删除堆顶元素(优先级最高的元素)。为了保持完全二叉树结构,步骤如下:

  1. 交换堆顶元素(下标 0)和最后一个元素(下标 size-1);
  2. size--(相当于删除了原堆顶元素);
  3. 对新堆顶(原最后一个元素)做向下调整,恢复堆结构。

代码实现

//删除堆顶元素,返回删除的值
public static int poll(int[] array, int size) {if (size == 0) {throw new NoSuchElementException("堆为空");}int top = array[0]; //保存堆顶值//1. 交换堆顶和最后一个元素array[0] = array[size - 1];//2. 调整新堆顶shiftDown(array, size - 1, 0);return top;
}

四、手动模拟优先级队列

基于上面的堆操作,我们可以封装一个简单的优先级队列(小根堆):

public class MyPriorityQueue {private int[] data; //存储元素private int size;   //有效元素个数private static final int DEFAULT_CAPACITY = 11; //默认容量(参考JDK)//构造器:默认容量public MyPriorityQueue() {data = new int[DEFAULT_CAPACITY];}//构造器:指定初始容量public MyPriorityQueue(int initialCapacity) {if (initialCapacity < 1) {throw new IllegalArgumentException("容量不能小于1");}data = new int[initialCapacity];}//插入元素public boolean offer(int value) {//简化:实际需扩容(比如数组满了就扩)if (size == data.length) {throw new IllegalStateException("队列已满");}data[size] = value;shiftUp(data, size + 1, size);size++;return true;}//删除并返回堆顶public int poll() {if (isEmpty()) {throw new NoSuchElementException("队列为空");}int top = data[0];data[0] = data[size - 1];shiftDown(data, --size, 0);return top;}//查看堆顶public int peek() {if (isEmpty()) {throw new NoSuchElementException("队列为空");}return data[0];}//判断是否为空public boolean isEmpty() {return size == 0;}//向下调整(小根堆)public static void shiftDown(int[] array, int size, int parent) {int child = 2 * parent + 1; //先找左孩子while (child < size) { //孩子存在才需要调整//1. 找左右孩子中更小的那个if (child + 1 < size && array[child + 1] < array[child]) {child++; //右孩子更小,更新child}//2. 比较父节点和最小孩子if (array[parent] <= array[child]) {break; //父节点更小,堆有序} else {//交换父节点和孩子int temp = array[parent];array[parent] = array[child];array[child] = temp;//继续向下调整(子树可能被破坏)parent = child;child = 2 * parent + 1;}}}//向上调整(小根堆)public static void shiftUp(int[] array, int size, int child) {int parent = (child - 1) / 2;while (child > 0) { //孩子不是根节点才需要调整if (array[child] >= array[parent]) {break; //堆有序} else {//交换int temp = array[child];array[child] = array[parent];array[parent] = temp;//继续向上child = parent;parent = (child - 1) / 2;}}}
}

五、Java 中的 PriorityQueue

        JDK 已经帮我们实现了优先级队列 java.util.PriorityQueue,无需重复造轮子。我们需要掌握它的核心特性和用法。

5.1 核心特性

  1. 线程不安全:多线程场景需用 PriorityBlockingQueue
  2. 元素需可比较:插入的元素必须实现 Comparable 接口,或创建队列时传入 Comparator,否则抛 ClassCastException
  3. 禁止存 null:插入 null 会抛 NullPointerException
  4. 默认小根堆:默认按元素自然顺序(Comparable)排序,堆顶是最小值;
  5. 自动扩容:无固定容量,满了会自动扩容;
  6. 时间复杂度:插入(offer)、删除(poll)均为 O(logN)

5.2 常见构造器与基础使用

PriorityQueue 提供多种构造器,常用的有 3 种:

构造器功能
PriorityQueue()默认容量 11,小根堆
PriorityQueue(int initialCapacity)指定初始容量,小根堆
PriorityQueue(Collection<? extends E> c)用集合初始化

基础用法示例

import java.util.ArrayList;
import java.util.PriorityQueue;public class PriorityQueueDemo {public static void main(String[] args) {//1. 空队列(默认容量11,小根堆)PriorityQueue<Integer> q1 = new PriorityQueue<>();q1.offer(5);q1.offer(2);q1.offer(8);System.out.println("q1堆顶:" + q1.peek()); //2(小根堆,最小值在顶)System.out.println("q1删除堆顶:" + q1.poll()); //2System.out.println("q1新堆顶:" + q1.peek()); //5//2. 用集合初始化ArrayList<Integer> list = new ArrayList<>();list.add(4);list.add(1);list.add(3);PriorityQueue<Integer> q2 = new PriorityQueue<>(list);System.out.println("q2大小:" + q2.size()); //3System.out.println("q2堆顶:" + q2.peek()); //1}
}

5.3 实现大堆:自定义比较器

默认是小根堆,若要实现大堆,需在创建队列时传入 Comparator(自定义排序规则)。有两种方式:

方式 1:匿名内部类
//大堆:比较器返回o2 - o1(o2大则返回正数,o2排在前面)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return o2 - o1; //o2大则优先级高}
});maxHeap.offer(5);
maxHeap.offer(2);
maxHeap.offer(8);
System.out.println(maxHeap.peek()); //8(大堆顶是最大值)
方式 2:Lambda 表达式(Java 8+,更简洁)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);

5.4 扩容机制

PriorityQueue 的扩容逻辑(JDK 1.8):

  • 若当前容量 < 64:扩容为原容量的 2 倍 + 2(比如 11→24,24→50);
  • 若当前容量 ≥ 64:扩容为原容量的 1.5 倍(比如 64→96,96→144);
  • 若扩容后超过 Integer.MAX_VALUE - 8:直接用 Integer.MAX_VALUE

六、堆的经典应用

        堆的应用非常广泛,除了实现优先级队列,还有两个高频场景:堆排序和 Top-K 问题。

6.1 堆排序

堆排序利用堆的性质实现高效排序,核心分两步:

  1. 建堆:升序排序建大堆(堆顶是最大值,方便放到末尾),降序排序建小堆
  2. 排序:循环将堆顶(最大值)与最后一个元素交换,然后对新堆顶做向下调整,直到所有元素有序。

代码实现(升序排序)

public class HeapSort {public static void sort(int[] array) {if (array == null || array.length < 2) {return;}int size = array.length;//步骤1:建大堆for (int parent = (size - 2) / 2; parent >= 0; parent--) {shiftDownMax(array, size, parent);}//步骤2:排序(循环交换堆顶和最后一个元素,调整堆)while (size > 1) {//交换堆顶(最大值)和最后一个元素int temp = array[0];array[0] = array[size - 1];array[size - 1] = temp;size--; //已排序元素不再参与堆调整//调整新堆顶shiftDownMax(array, size, 0);}}//大堆的向下调整private static void shiftDownMax(int[] array, int size, int parent) {int child = 2 * parent + 1;while (child < size) {//找左右孩子中更大的那个if (child + 1 < size && array[child + 1] > array[child]) {child++;}//父节点 >= 孩子,堆有序if (array[parent] >= array[child]) {break;}//交换int temp = array[parent];array[parent] = array[child];array[child] = temp;parent = child;child = 2 * parent + 1;}}//测试public static void main(String[] args) {int[] array = {5, 1, 7, 2, 3, 17};sort(array);System.out.println(Arrays.toString(array)); //[1, 2, 3, 5, 7, 17]}
}

时间复杂度:建堆 O(N) + 排序 O(NlogN),整体 O(NlogN)

6.2 Top-K 问题

        Top-K 问题是指:从海量数据中找出前 K 个最大 / 最小的元素(比如从 100 万个数中找前 100 个最大的数)。

        直接排序的问题:数据量大时(如 10 亿个数据),无法全部加载到内存,且排序 O(NlogN) 效率低。堆是最优解,思路如下:

找前 K 个最大的元素:建小堆
  1. 用前 K 个元素建小堆(堆顶是这 K 个元素中最小的,也是 “守门人”);
  2. 遍历剩余 N-K 个元素:
    • 若元素 > 堆顶:替换堆顶,然后向下调整(保证堆顶仍是 K 个中最小的);
    • 若元素 ≤ 堆顶:跳过(不可能是前 K 大);
  3. 遍历结束后,堆中 K 个元素就是前 K 个最大的。
找前 K 个最小的元素:建大堆

逻辑类似,只是用前 K 个元素建大堆,剩余元素 < 堆顶时替换并调整。

代码实现(找前 K 个最小元素)

import java.util.PriorityQueue;public class TopK {//从arr中找前k个最小的元素public static int[] smallestK(int[] arr, int k) {if (arr == null || k <= 0 || k > arr.length) {return new int[0];}//步骤1:用前k个元素建大堆(堆顶是k个中最大的,筛选更小的元素)PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);for (int i = 0; i < k; i++) {maxHeap.offer(arr[i]);}//步骤2:遍历剩余元素,比堆顶小则替换for (int i = k; i < arr.length; i++) {if (arr[i] < maxHeap.peek()) {maxHeap.poll(); //移除堆顶(当前k个中最大的)maxHeap.offer(arr[i]); //加入更小的元素}}//步骤3:将堆中元素存入结果数组int[] result = new int[k];for (int i = 0; i < k; i++) {result[i] = maxHeap.poll();}return result;}//测试public static void main(String[] args) {int[] arr = {3, 1, 4, 1, 5, 9, 2, 6};int[] top3 = smallestK(arr, 3);System.out.println(Arrays.toString(top3)); // [1, 1, 2]}
}

时间复杂度:建堆 O(KlogK) + 遍历剩余元素 O((N-K)logK),整体 O(NlogK),远优于排序的 O(NlogN)

总结

堆和优先级队列是 Java 开发中的 “常客”,核心要点可以概括为:

  1. 优先级队列是 “按优先级出队” 的队列,底层依赖堆实现;
  2. 堆是完全二叉树,分小根堆(顶小)和大根堆(顶大),用数组存储;
  3. 堆的核心操作是向下调整(建堆、删除)和向上调整(插入);
  4. PriorityQueue 默认小堆,需自定义比较器实现大堆;
  5. 堆的经典应用:堆排序(O(NlogN))、Top-K 问题(O(NlogK))。

掌握这些知识点,不仅能应对面试中的高频问题,也能在实际开发中(如任务调度、数据筛选)灵活运用。如果有疑问,建议多动手写代码,模拟堆的调整过程,加深理解!

http://www.dtcms.com/a/537598.html

相关文章:

  • vps做网站用什么系统wordpress文库
  • DeepSeek-OCR:革命性文档识别模型全面解析及实测
  • 《自动控制原理》第 3 章 线性控制系统的运动分析:3.4
  • csdn_export_md
  • 十大纯净系统网站微分销系统是什么
  • 深入剖析平台设备驱动与设备树匹配机制
  • __金仓数据库平替MongoDB实战:以电子证照系统为例__
  • 2.2.1.11 大数据方法论与实践指南-数据链路依赖追踪实践
  • 临沂供电公司网站企业网站有什么功能
  • 网站做前端汕头seo排名收费
  • 旅游型网站建设河北seo网站开发
  • 中文网站什么意思做网站必须先买域名吗
  • Boosting家族 -- XGBoost分享
  • win服务器做网站做外贸的国际网站有哪些
  • 重庆市建设工程信息网站诚信分wordpress计费搜索
  • “心灵灯塔”AI辅助心理自我干预全流程指南
  • 学会网站 建设广州网站设计公司哪里济南兴田德润怎么联系
  • 网站结构优化包括什么查找企业资料的网站
  • 高并发购物商城系统搭建实战
  • [人工智能-大模型-95]:大模型应用层 - RAG, 大模型,应用之间的关系
  • CrossFormer 论文详解教程
  • 【Java】@Autowired警告问题,使用@RequiredArgsConstructor
  • 免费推广网站2023网页设计实训心得500字
  • wordpress快站怎么样哪一个做h5的网站好
  • 大模型入门学习路径(个人学习路径的分享)
  • 医药平台网站建设国外h5制作网站模板
  • wordpress博客收录查询西安网站优化公司
  • Docker基础教程 - 容器化部署入门指南
  • 谷歌网站开发用什么框架软件技术专升本需要考什么
  • 企业百度网站建设做模具行业的网站