1.优先级队列
1.1概念
优先级队列(Priority Queue) 是一种抽象数据类型(ADT),其行为类似于普通的队列或栈,但有一个关键区别:每一个元素都关联有一个“优先级”(Priority)。
- 出队顺序:元素出队(被删除)的顺序不是由它们进入队列的“时间”决定,而是由它们的优先级决定。
- 最高优先级先出:优先级最高的元素最先被服务(即出队)。
- 进入顺序无关:优先级低的元素即使比优先级高的元素更早进入队列,也必须等到所有优先级更高的元素都出队后才能被服务。
你可以把它想象成医院急诊室。病人(元素)并不是严格按照先来后到的顺序接受治疗(出队),而是由护士根据病情的紧急程度(优先级)进行分诊。一个突发心脏病的病人(高优先级)即使刚来,也会比一个已经等了两个小时但只是手指划伤的病人(低优先级)优先得到救治。
2.优先级队列的模拟实现
2.1 堆的概念
堆是一种特殊的、基于树的、满足某些特定性质的完全二叉树。它主要用于实现优先级队列这种抽象数据类型。
堆主要有两种类型,其核心区别在于堆的性质:
- 最大堆(Max-Heap):
- 性质:在任何子树中,根节点的值都大于或等于其所有子节点的值。
- 结果:堆中的最大值始终位于根节点。
- 最小堆(Min-Heap):
- 性质:在任何子树中,根节点的值都小于或等于其所有子节点的值。
- 结果:堆中的最小值始终位于根节点。
关键特性
- 完全二叉树(Complete Binary Tree):
- 这是堆的结构特性。意味着树的所有层都是完全填充的,除了最后一层,最后一层的节点都尽可能地集中在左边。
- 这个特性使得堆可以非常高效地使用数组或列表来实现,而不需要像普通二叉树那样用节点和指针。对于数组中位置
i的节点:- 其父节点的位置:
parent(i) = (i - 1) // 2 - 其左子节点的位置:
left_child(i) = 2*i + 1 - 其右子节点的位置:
right_child(i) = 2*i + 2
- 其父节点的位置:
- 堆序性(Heap Order Property):
- 这是堆的顺序特性,即上面提到的最大堆或最小堆的性质。
- 这个特性保证了我们能在常数时间
O(1)内找到最大或最小元素(直接取根节点)。
2.2 堆的存储方式
从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储,
注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节 点,就会导致空间利用率比较低。
可以根据二叉树知识:假设i为节点在数组中的下标,则有:
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
2.3 堆的创建
2.3.1 堆向下调整
方法一:自顶向下插入法(Heapify-Up)
这种方法最为直观,类似于我们一个一个地向空堆中添加元素。
算法步骤:
- 从一个空堆开始。
- 遍历输入数组中的每一个元素。
- 对于每个元素,执行以下操作:
- 将新元素追加到堆数组的末尾。
- 对新元素执行
sift_up(上浮) 操作,使其“游”到正确的位置,以恢复堆的性质。
private void siftDown(int parent, int usedSize){
int child = 2 * parent+1;
while(child < usedSize){
//判断左右孩子的最大值
if (child+1 < usedSize && elem[child] < elem[child+1]){
child++;
}
if (elem[child] > elem[parent]){
swap(elem,child,parent);
parent = child;
child = 2 * parent + 1;
}else {
break;
}
}
}
**注意:**在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下调整。
时间复杂度分析: 最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为 O(logN)
2.3.2 堆的创建
public void createHeap(){
for (int parent = (this.usedSize - 1)/2; parent >= 0 ; parent--) {
siftDown(parent,this.usedSize);
}
}
建堆的时间复杂度:O(N)
2.4 堆的插入与删除
插入操作的目标是向堆中添加一个新元素,同时保持堆的性质不变。
堆的插入(Insertion / Push)
算法步骤:
- 添加到末尾:将新元素插入到堆数组的最后一个位置(完全二叉树的最后一个节点)。
- 上浮调整(Sift Up):将新元素与其父节点进行比较:
- 如果它大于父节点(违反最大堆性质),则与父节点交换位置。
- 重复这个过程,直到新元素不再大于其父节点,或者到达根节点为止。
堆的删除(Deletion / Pop)
删除操作通常指的是删除并返回堆中的最大元素(最大堆的根节点)。
算法步骤:
- 保存根节点:记录根节点的值(这是要返回的最大值)。
- 移动末尾元素:将堆数组的最后一个元素移动到根节点的位置。
- 删除末尾:从数组中移除最后一个元素(现在已移动到根节点)。
- 下沉调整(Sift Down):从新的根节点开始,将其与两个子节点中较大的那个进行比较:
- 如果它小于较大的子节点,则与那个子节点交换位置。
- 重复这个过程,直到它大于或等于它的两个子节点,或者到达叶子节点为止。
- 返回最大值:返回步骤1中保存的根节点值。
2.5 用堆模拟实现优先级队列
public class MyPriorityQueue {
// 演示作用,不再考虑扩容部分的代码
private int[] array = new int[100];
private int size = 0;
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];
}
}
3.常用接口介绍
3.1 PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线 程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
关于PriorityQueue的使用要注意:
- 使用时必须导入PriorityQueue所在的包
- PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常
- 不能插入null对象,否则会抛出NullPointerException
- 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
- 插入和删除元素的时间复杂度为
- PriorityQueue底层使用了堆数据结构
- PriorityQueue默认情况下是小堆—即每次获取到的元素都是最小的元素
3.2 PriorityQueue常用接口介绍
1.优先级队列的构造
| 构造器 | 功能介绍 |
|---|---|
| PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
| PriorityQueue(int initialCapacity) | 创建一个初始容量为initialCapacity的优先级队列,注意: initialCapacity不能小于1,否则会抛IllegalArgumentException异常 |
| PriorityQueue(Collection c) | 用一个集合来创建优先级队列 |
注意:默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器
2.插入/删除/获取优先级最高的元素
| 函数名 | 功能介绍 |
|---|---|
| boolean offer(E e) | 插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 ,注意:空间不够时候会进行扩容 |
| E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
| E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
| int size() | 获取有效元素的个数 |
| void clear() | 清空 |
| boolean isEmpty() | 检测优先级队列是否为空,空返回true |
优先级队列的扩容说明:
- 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
- 如果容量大于等于64 ,是按照oldCapacity的1.5倍方式扩容 的
- 如果容量超过MAX ARRAY_ SIZ E ,按照MAX ARRAY_ SIZ E来进行扩容
3.3.oj练习
top-k问题:最大或者最的前k个数据 。
public int[] samllestK(int[] arr,int k){
int[] ret = new int[k];
if (arr == null || k == 0){
return ret;
}
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(k,new IntCmp());
//k*logk
for (int i = 0; i < k; i++) {
priorityQueue.offer(arr[i]);
}
//(n-k)*logk
for (int i = k; i < arr.length; i++) {
int peekVal = priorityQueue.peek();
if (arr[i] < peekVal){
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
for (int i = 0; i < k; i++) {
ret[i] = priorityQueue.poll();
}
return ret;
}
4.堆的应用
4.1 PriorityQueue的实现
用堆作为底层结构封装优先级队列
4.2 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
-
建堆升序:建大堆降序:建小堆
-
利用堆删除思想来进行排序 建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
4.3 Top-k问题 TOP-K问题:
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都 不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆 前k个最大的元素,则建小堆 前k个最小的元素,则建大堆
- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
public int[] smallestK1(int[] arr,int k){
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
//O(N*logN)
for (int i = 0; i < arr.length; i++) {
priorityQueue.offer(arr[i]);
}
//O(K*logN)
int[] ret = new int[k];
for (int i = 0; i < k; i++) {
ret[i] = priorityQueue.poll();
}
return ret;
}
926

被折叠的 条评论
为什么被折叠?



