【数据结构】PriorityQueue优先队列:基于堆(heap)实现
要理解 PriorityQueue(优先队列),核心是先搞懂「堆(heap)」这个底层数据结构,再看队列如何用堆实现“按优先级排序”
一、先搞懂
PriorityQueue 是干嘛的?
“普通队列” 是 先进先出 (FIFO),谁先来谁先走。
“优先队列” 则不同,它是 谁 “优先级” 高谁先走。
医院挂号队列
- 普通病人排队看医生(FIFO)
- 急诊病人优先(Priority)
所以 PriorityQueue 本质上就是:
一种能“随时插入”,但“取出时总是取最高优先级”的结构。
堆(heap)到底是什么?
堆不是“垃圾堆”,而是一种 “有序的完全二叉树”(可以简单理解成“长得很规整、且有大小规则的树”),主要分两种:
- 大顶堆:树的“根节点”是最大的,且每个父节点都比自己的子节点大(像一个金字塔,顶端是最大的);
- 小顶堆:树的“根节点”是最小的,且每个父节点都比自己的子节点小(像一个倒金字塔,顶端是最小的)。
堆的核心特点(通俗版):
- “规整”:完全二叉树 = 除了最后一层,每一层的节点都排满了;最后一层的节点从左到右依次排列,不跳空(比如3层树,第1层1个节点,第2层2个,第3层先排左边2个,再排右边,不会左边空着先排右边);
- “有规矩”:父节点和子节点必须满足“大顶/小顶”规则(比如大顶堆里,爸爸永远比儿子大,不会出现“儿子比爸爸大”的情况);
- “好维护”:添加/删除节点后,能快速调整回“规整+有规矩”的状态(这个调整过程叫“上浮”或“下沉”,后面会说)。
堆的可视化(小顶堆例子):
1 (根节点,最小)/ \3 2 (父节点3>1,2>1,符合规则)/ \ /
4 5 6 (父节点4>3,5>3,6>2,符合规则)
这就是一个小顶堆,顶端永远是最小的元素,且结构规整。
二、PriorityQueue:用堆实现“按优先级出队”
普通队列是「先进先出(FIFO)」——比如排队买奶茶,先到的先买;
但优先队列是「优先级高的先出」——比如医院急诊,不管谁先来,病情重的先救治。
而 堆就是优先队列的“底层容器”:用大顶堆实现“最大元素优先出队”,用小顶堆实现“最小元素优先出队”(Java的PriorityQueue默认是小顶堆)。
核心逻辑:堆的“顶端”就是“当前优先级最高的元素”
因为堆的规则(大顶堆顶端最大,小顶堆顶端最小),每次要出队时,直接取堆顶元素就行——这是最高效的方式(时间复杂度O(1))。
关键是:添加/删除元素后,如何保持堆的规则? 用两个核心操作:
三、两个关键操作:保证堆的“规矩”不被破坏
假设我们用「小顶堆」实现优先队列(优先出最小元素),看两个常见场景:
1. 新增元素(入队):上浮操作
比如我们往上面的小顶堆里加一个元素「0」,步骤:
- 先把新元素放到堆的“最后一个位置”(保证结构是完全二叉树,不破坏“规整”):
1/ \3 2/ \ / \ 4 5 6 0 (新元素0放在最后) - 发现“儿子(0)比爸爸(2)小”,违反小顶堆规则——需要“上浮”:把儿子和爸爸交换位置,直到符合规则:
- 第一次交换(0和2):
1/ \3 0 (爸爸2和儿子0交换)/ \ / \ 4 5 6 2 - 再检查“儿子(0)比爷爷(1)小”,继续交换:
0 (新的根节点,最小元素)/ \3 2/ \ / \ 4 5 6 2
- 第一次交换(0和2):
2. 删除元素(出队):下沉操作
优先队列出队只能出“堆顶元素”(优先级最高的),比如把上面的堆顶「0」删掉,步骤:
- 先把堆的“最后一个元素”放到堆顶(填补空位,保证结构是完全二叉树):
2 (最后一个元素2移到堆顶)/ \3 2/ \ / \ 4 5 6 (原来的最后一个位置空了,结构仍规整) - 发现“爸爸(2)比儿子(3和2)中的最小儿子(2)相等,比另一个儿子(3)小”——这里假设儿子中有比爸爸小的,需要“下沉”:把爸爸和最小的儿子交换位置,直到符合规则:
- 这里堆顶2的两个儿子是3和2,最小的儿子是2,交换后:
2 (堆顶还是2,和儿子交换后没变化)/ \3 2/ \ / \ 4 5 6
- 这里堆顶2的两个儿子是3和2,最小的儿子是2,交换后:
- 此时堆的规则恢复,新的堆顶是2(当前最小元素),下次出队就出它。
四、总结:PriorityQueue的核心逻辑
- 底层用「堆」(完全二叉树+大小规则)存储元素,堆顶永远是“当前优先级最高的元素”(小顶堆→最小,大顶堆→最大);
- 入队:新元素放最后,然后“上浮”到合适位置,保持堆规则;
- 出队:只出堆顶元素,然后用最后一个元素补位,再“下沉”到合适位置,保持堆规则;
- 优势:不管入队还是出队,调整堆的时间复杂度都是O(log n)(比普通数组排序快多了),适合需要“动态按优先级取元素”的场景(比如任务调度、Top K问题)。
五、生活化类比
把优先队列想象成「公司的任务池」:
- 堆 = 任务排序表,规定“优先级高的任务(比如紧急bug)放在最前面”;
- 入队 = 新增一个任务,会自动按优先级插入到排序表的合适位置(不会乱);
- 出队 = 每次只拿最紧急的任务(排序表第一个,对应堆顶);
- 上浮/下沉 = 新增/完成任务后,自动调整排序表,保证最紧急的任务永远在第一个。
六、其他实现方式对比
| 实现方式 | 插入 | 删除最值 | 是否全局有序 | 特点 |
|---|---|---|---|---|
| 有序数组/有序链表 | O(n) | O(1) | ✅ 是 | 插入慢、取出快 |
| 堆(Heap) | O(log n) | O(log n) | ❌ 否 | 插入删都快、部分有序(最常用) |
