目录
一、优先级队列
1、概念
2、优先级队列的模拟实现
2.1 堆的概念
2.2 堆的存储方式
2.3 堆的创建
2.3.1 堆向下调整
2.3.2 堆的创建
2.4 堆的插入与删除
2.4.1 堆的插入
2.4.2 堆的删除
2.5 用堆模拟实现优先级队列
3.常用接口介绍
3.1 PriorityQueue的特性
3.2 PriorityQueue常用接口介绍
1. 优先级队列的构造
2. 插入/删除/获取优先级最高的元素
3.3 top-k问题
一、优先级队列
1、概念
我们都知道队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适。(例如,我们在用手机玩游戏的时候,有个电话打过来,那么系统应该优先处理打进来的电话)
2、优先级队列的模拟实现
在JDK1.8中,PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。
2.1 堆的概念
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
2.2 堆的存储方式
从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。
2.3 堆的创建
2.3.1 堆向下调整
我们知道堆是在二叉树的基础上进行了调整,堆的存储结构是将节点按照层序遍历的方式放到一数组中。所以现在我们先把这一集合构造为二叉树的形式,再来用向下调整的方式,构造出堆,这里我们用小根堆。
仔细观察上图可发现:根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。
- parent右孩子是否存在,若存在,则找到左右孩子中最小的孩子,让child进行标记
- 将parent与较小的孩子child比较,如果:
- parent小于较小的孩子child,调整结束
- 否则:交换parent与较小的孩子child,交换完成之后,由于较大的元素向下移动,可能导致子树不满足堆的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续(2)。
代码如下:
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 t = array[parent];
array[parent] = array[child];
array[child] = t;
// parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
parent = child;
child = parent * 2 + 1;
}
}
}
2.3.2 堆的创建
那对于普通的序列{ 1,5,3,8,7,6 },即根节点的左右子树不满足堆的特性,又该如何调整呢?
此时可以找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整。
代码如下:
public static void createHeap(int[] array) {
// 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
int root = ((array.length-2)>>1); //(array.length-1-1)/2
for (; root >= 0; root--) {
shiftDown(array, root); // shiftDown方法在创建大根堆时和小根堆不一样,注意
}
}
我们来看一下创建大根堆的例子:
建堆的时间复杂度为O(N).
2.4 堆的插入与删除
2.4.1 堆的插入
- 先将元素放入到底层空间中(注意:空间不够时需要扩容)
- 将最后新插入的节点向上调整,直到满足堆的性质
可以以下图参考该过程:
代码(大根堆为例):
public 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;
}
}
}
2.4.2 堆的删除
- 将堆顶元素对堆中最后一个元素交换
- 将堆中有效数据个数减少一个
- 对堆顶元素进行向下调整
如下图:
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 是线程安全的。
import java . util . PriorityQueue ;
3.2 PriorityQueue常用接口介绍
1. 优先级队列的构造
构造器 | 功能介绍 |
PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(int initialCapacity)
|
创建一个初始容量为
initialCapacity
的优先级队列,注意:
initialCapacity
不能小于
1
,否则会抛
IllegalArgumentException
异
常
|
PriorityQueue(Collection<?
extends E> c)
| 用一个集合来创建优先级队列 |
static void TestPriorityQueue(){
// 创建一个空的优先级队列,底层默认容量是11
PriorityQueue<Integer> q1 = new PriorityQueue<>();
// 创建一个空的优先级队列,底层的容量为initialCapacity
PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
ArrayList<Integer> list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
// 用ArrayList对象来构造一个优先级队列的对象
// q3中已经包含了三个元素
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
}
注意:默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器
// 用户自己定义的比较器:直接实现Comparator接口,然后重写该接口中的compare方法即可
class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
}
public class TestPriorityQueue {
public static void main(String[] args) {
PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
p.offer(4);
p.offer(3);
p.offer(2);
p.offer(1);
p.offer(5);
System.out.println(p.peek());
}
}
2. 插入/删除/获取优先级最高的元素
函数名 | 功能介绍 |
boolean offer(E e)
|
插入元素
e
,插入成功返回
true
,如果
e
对象为空,抛出
NullPointerException
异常,时
间复杂度 O(logN),
注意:空间不够时候会进行扩容
|
E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size() | 获取有效元素的个数 |
void clear()
| 清空 |
boolean
isEmpty()
| 检测优先级队列是否为空,空返回true |
- 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
- 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
- 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
3.3 top-k问题
top-k问题:最大或者最小的前k个数据。
题目链接:https://leetcode.cn/problems/smallest-k-lcci/description/https://leetcode.cn/problems/smallest-k-lcci/description/代码:
class Solution {
public int[] smallestK(int[] arr, int k) {
// 参数检测
if(null == arr || k <= 0)
return new int[0];
PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
// 将数组中的元素依次放到堆中
for(int i = 0; i < arr.length; ++i){
q.offer(arr[i]);
}
// 将优先级队列的前k个元素放到数组中
int[] ret = new int[k];
for(int i = 0; i < k; ++i){
ret[i] = q.poll();
}
return ret;
}
}
该解法只是PriorityQueue的简单使用,并不是topK最好的做法。
1. 用数据集合中前 K 个元素来建堆前k个最大的元素,则建小堆前k个最小的元素,则建大堆2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。
代码:
//使用比较器创建大根堆
class comp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
}
class Solution {
public int[] smallestK(int[] arr, int k) {
int[] ret = new int[k];
if(arr == null || k <= 0) {
return ret;
}
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new comp());
for (int i = 0;i < k ;i++) {
priorityQueue.offer(arr[i]);
}
for (int i = k;i < arr.length;i++) {
int top = priorityQueue.peek();
if (arr[i] < top) {
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
int[] res = new int[k];
for (int i = 0; i < k;i++) {
res[i] = priorityQueue.poll();
}
return res;
}
}
对堆的内容就先介绍到这儿了,希望能帮到你呀!