文章目录
- 一、堆
- 1.1 堆的概念
- 1.2 堆的存储方式
- 1.3 堆的创建
- 1.4 堆的插入与删除
- 1.5 堆的应用场景
- 二、 优先级队列
- 2.1 什么是优先级队列
- 2.2 堆模拟实现优先级队列
- 三、Java中的PriorityQueue
- 3.1 PriorityQueue的特性
- 3.2 常用方法
一、堆
1.1 堆的概念
在数据结构中,堆(Heap)是一种特殊的树形结构。它总是一颗完全二叉树,其父节点的值大于或等于(大堆)或者小于或者等于(小堆)其子节点的值。
例如下图所展示的小堆和大堆:
堆的主要特点是:
- 在大堆中,根节点的值始终大于或等于其子节点的值;
- 在小堆中,根节点的值始终小于或等于其子节点的值;
根据这种性质就能够帮助我们快速的找到堆中的最大值或者最小值。优先级队列就是有堆来实现的,其中元素的访问和删除操作都是基于元素的优先级。在大堆中,根节点始终是优先级最高的元素,可以快速找到和删除最大元素。同样,在小堆中,根节点是优先级最低的元素。
1.2 堆的存储方式
从以上堆的概念可知,堆是一棵完全二叉树,因此可以采用数组,以层序遍历的顺序对元素进行储存:
需要注意的是,对于非完全二叉树则不适合采用数组顺序存储,因为为了能够还原出二叉树,数组中还必须要存储空节点,这样势必就会浪费一定的空间了。
此外,将堆中的元素存储到数组之后,可可以根据二叉树的性质对树进行还原。假设 i
为节点在数组中的下标,则有:
- 如果
i
为 0,则表示 i 下标的节点为根节点;若不为 0 ,则其父节点的下标为:(i - 1) / 2
。- 如果
2 * i + 1
小于总节点个数,则节点i
的左孩子节点小标为2 * i + 1
,否则就没有左孩子节点。- 如果
2 * i + 2
小于总节点个数,则节点i
的右孩子节点小标为2 * i + 1
,否则就没有右孩子节点。
1.3 堆的创建
1. 堆向下调整
对于数组{27, 15, 19, 18, 28, 34, 65, 49, 25, 37}
,如何使用数组中的元素建立一个小堆呢?
观察上图可以发现:根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。
向下调整的过程:
-
用
parent
标记当前需要调整的节点下标,用child
标记parent
的左孩子下标(注意:parent
如果有孩子一定先是有左孩子)。 -
如果
parent
的左孩子存在,即child < size
, 进行以下操作,直到parent
的左孩子不存在:parent
右孩子是否存在,如果存在,找到左右孩子中最小的孩子,用child
进行标记;- 将
parent
与较小的孩子child
进行比较,如果parent
小于较小的孩子child
,调整结束;否则交换parent
与child
节点,然后更新parent
为child
,child = 2 * parent + 1
,继续向下调整。
例如下面的调整流程图:
即最后当child
的值大于数组的长度length
的时候就调整完成了。
向下调整算法的代码如下:
public void shiftDown(int[] array, int parent) {
// child先标记parent的左孩子,因为parent可能右左没有右
int child = 2 * parent + 1;
int size = array.length;
while (child < size) {
// 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
if (child + 1 < size && array[child + 1] < array[child]) {
child += 1;
}
// 如果双亲比其最小的孩子还小,说明该结构已经满足堆的特性了
if (array[parent] <= array[child]) {
break;
} else {
// 将双亲与较小的孩子交换
int tmp = array[parent];
array[parent] = array[child];
array[child] = tmp;
// parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
parent = child;
child = parent * 2 + 1;
}
}
}
注意:在调整以parent
为根的二叉树时,必须要满足parent
的左子树和右子树已经是堆了才可以向下调整。
2. 堆的创建
那对于普通的序列{ 1, 5, 3, 8, 7, 6 }
要建立大堆,现在其根节点的左右子树不满足堆的特性,又该如何调整呢?
例如下面的流程图:
- 首先找到最后一个叶子节点的父节点,然后对其进行向下调整;
- 依次找当前父节点的前一个节点作为父节点,然后进行向下调整,直到找到根节点。
实现的代码如下:
public void createHeap(int[] array) {
// 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
for (int i = (array.length - 1 - 1) / 2; i >= 0; i--){
shiftDown(array, i);
}
}
1.4 堆的插入与删除
1. 堆的插入
- 先将元素放入到底层空间中(注意:空间不够时需要扩容);
- 将最后新插入的节点向上调整,直到满足堆的性质。
堆的向上调整算法如下:
public void shiftUp(int child, int[] array) {
// 找到child的双亲
int parent = (child - 1) / 2;
while (child > 0) {
// 如果双亲比孩子大,parent满足堆的性质,调整结束
if (array[parent] > array[child]) {
break;
} else {
// 将双亲与孩子节点进行交换
int t = array[parent];
array[parent] = array[child];
array[child] = t;
// 小的元素向下移动,可能到值子树不满足对的性质,因此需要继续向上调增
child = parent;
parent = (child - 1) / 2;
}
}
}
2. 堆的删除
堆的删除一定删除的是堆顶元素。具体规则如下:
- 将堆顶元素对堆中最后一个元素交换
- 将堆中有效数据个数减少一个
- 对堆顶元素进行向下调整
1.5 堆的应用场景
1. 实现优先级队列
2. 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
-
建堆
- 升序:建大堆
- 降序:建小堆
-
利用堆删除思想来进行排序
- 建堆和堆删除中都用到了向下调整,因此利用向下调整,就可以完成堆排序。
-
Top K 问题
例如:求最小K个元素
思路:
- 要解决Top K 问题,不能简单的对数组进行排序,然后直接去前K个元素,因为如果数据量非常大的情况下,就不能将全部数据都加载进内存,也就不能实现排序了。
- 因此就需要使用到优先级队列了。当然不是建立一个小堆,然后用全部的元素添加到堆中,最后弹出k个元素。那么这样的话时间复杂度就为
O(N + K * logN)
了。- 这里解决的思路就是:如果要求最小的K个元素,那么就建立一个大小为K的大堆,然后将数组中的前K个元素添加到大堆中,再遍历数组中剩下的N - 1 个元素,当遇到比堆顶元素大的就与堆顶元素进行交换。当遍历完整个数组的时候,最小的前K个元素就保存到堆中的。
- 这种方法的整个时间复杂度可以达到
O(N * logK)
,而 K 的值一般比较小。
class Solution{
public int[] smallestK(int[] arr, int k) {
int[] res = new int[k];
// k 为 0 时,PriorityQueue的构造方法会抛异常
if(arr == null || k == 0){
return res;
}
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
for (int i = 0; i < arr.length; i++) {
if(maxHeap.size() < k){
maxHeap.offer(arr[i]);
}
else {
if(arr[i] < maxHeap.peek()){
maxHeap.poll();
maxHeap.offer(arr[i]);
}
}
}
// System.out.println(maxHeap);
for (int i = 0; i < k; i++) {
res[i] = maxHeap.poll();
}
return res;
}
}
二、 优先级队列
2.1 什么是优先级队列
优先队列(Priority Queue)是一种特殊的队列数据结构,其中每个元素都关联有一个优先级。与普通队列不同,优先队列中的元素并不按照插入的顺序进行处理,而是根据其优先级进行排序和访问。
优先队列的主要特点是,当从队列中取出元素时,具有最高优先级的元素首先被访问和删除。这意味着高优先级的元素先于低优先级的元素被处理。对于相同优先级的元素,可以根据插入的顺序或其他规则来确定其访问顺序。
优先队列可以通过不同的数据结构来实现,常见的实现方式包括堆(Heap)、二叉搜索树(Binary Search Tree)等。其中,使用堆实现的优先队列最为常见和高效。堆可以快速访问和删除具有最高(或最低)优先级的元素,并保持队列的有序性质。
优先队列的常见操作包括插入元素、删除最高优先级元素、获取最高优先级元素等。插入操作将元素按照其优先级插入到队列中的适当位置,删除操作会删除并返回具有最高优先级的元素,获取操作则返回但不删除最高优先级的元素。
优先队列在很多应用中都有广泛的应用,例如任务调度、图算法(如Dijkstra算法、Prim算法)等。它提供了一种方便的方式来处理具有优先级的元素,使得高优先级的任务或对象可以更快地被处理和访问。
2.2 堆模拟实现优先级队列
public class MyPriorityQueue {
// 演示作用,不再考虑扩容部分的代码
private int[] array = new int[100];
private int size = 0;
private void shiftUp(int child) {
// 找到child的双亲
int parent = (child - 1) / 2;
while (child > 0) {
// 如果双亲比孩子大,parent满足堆的性质,调整结束
if (array[parent] > array[child]) {
break;
} else {
// 将双亲与孩子节点进行交换
int t = array[parent];
array[parent] = array[child];
array[child] = t;
// 小的元素向下移动,可能到值子树不满足对的性质,因此需要继续向上调增
child = parent;
parent = (child - 1) / 2;
}
}
}
private void shiftDown(int parent) {
// child先标记parent的左孩子,因为parent可能右左没有右
int child = 2 * parent + 1;
int size = array.length;
while (child < size) {
// 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
if (child + 1 < size && array[child + 1] < array[child]) {
child += 1;
}
// 如果双亲比其最小的孩子还小,说明该结构已经满足堆的特性了
if (array[parent] <= array[child]) {
break;
} else {
// 将双亲与较小的孩子交换
int tmp = array[parent];
array[parent] = array[child];
array[child] = tmp;
// parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
parent = child;
child = parent * 2 + 1;
}
}
}
public void offer(int e) {
array[size++] = e;
shiftUp(size - 1);
}
public int poll() {
int oldValue = array[0];
array[0] = array[--size];
shiftDown(0);
return oldValue;
}
public int peek() {
return array[0];
}
}
三、Java中的PriorityQueue
3.1 PriorityQueue的特性
在Java中,PriorityQueue
是一个实现了优先级队列的类。它是基于堆(Heap)数据结构实现的,具有以下特性:
-
优先级顺序:元素根据其优先级进行排序。默认情况下,元素按照自然顺序进行排序,或者可以通过提供自定义的比较器(
Comparator
)来定义元素的排序规则。 -
自动调整:当插入或删除元素时,
PriorityQueue
会自动进行调整以保持堆的性质。插入元素时,会根据优先级将元素放入合适的位置;删除元素时,会移除堆顶元素,并重新调整堆以保持堆的性质。 -
快速访问:
PriorityQueue
提供了快速访问具有最高(或最低)优先级的元素。通过peek()
方法可以获取堆顶的元素,而不会删除它;通过poll()
方法可以获取并删除堆顶的元素。 -
元素重复:
PriorityQueue
允许元素重复。如果多个元素具有相同的优先级,它们的相对顺序可能会根据插入的顺序而不同。 -
动态大小:
PriorityQueue
具有动态调整大小的能力。它会根据需要自动扩展或收缩底层数组的大小,以容纳更多或更少的元素。
需要注意的是,PriorityQueue
是非线程安全的,不适用于多线程环境。如果在多线程环境中使用,应该采取适当的同步措施。
PriorityQueue
是Java提供的一个常用的数据结构,广泛应用于任务调度、事件处理、最短路径算法等场景,方便地处理具有优先级的元素。
3.2 常用方法
PriorityQueue 实现了 Queue 接口并提供了一些额外的方法来支持优先级队列的操作。下面是一些常用的 PriorityQueue 方法:
- add(E element) / offer(E element):将指定元素插入队列。如果队列已满,add 方法会抛出异常,而 offer 方法会返回 false。
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(5);
pq.offer(3);
pq.offer(7);
- remove() / poll():移除并返回队列中的头部元素(具有最高优先级)。如果队列为空,remove 方法会抛出异常,而 poll 方法会返回 null。
int head = pq.remove();
int head = pq.poll();
- peek():返回队列中的头部元素(具有最高优先级),但不移除它。如果队列为空,返回 null。
int head = pq.peek();
- size():返回队列中的元素数量。
int size = pq.size();
- isEmpty():判断队列是否为空。
boolean empty = pq.isEmpty();
下面是一个完整的示例,演示了如何使用 PriorityQueue 来实现优先级队列:
import java.util.PriorityQueue;
public class PriorityQueueExample {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(3);
pq.offer(7);
pq.offer(1);
while (!pq.isEmpty()) {
int head = pq.poll();
System.out.println("Element: " + head);
}
}
}
输出结果为:
Element: 1
Element: 3
Element: 5
Element: 7