一、优先级队列(PriorityQueue)
1、概念:
前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队(PriorityQueue)。
2、PriorityQueue的特性:
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
3、注意事项:
- 使用时必须导入 PriorityQueue 所在的包;
- PriorityQueue中放置的元素必须要能够比较大小 (只有实现了 Comparable 和 Comparator 接口的类才能比较大小),不能插入无法比较大小的对象,否则会抛出 ClassCastException 异常
- 不能插入 null 对象,否则会抛出 NullPointerException 异常
- 插入和删除元素的时间复杂度均为 O(logn)
- PriorityQueue底层使用了堆数据结构
- PriorityQueue默认情况下是小堆
4、常用的方法:
4.1、常用的构造方法:
4.2、常用的方法:
5、优先级队列的应用:
5.1、topk问题:Leetcode—topK
5.2、思路:将数组所有元素放到优先级队列当中,然后取前 k 个;
5.3、代码实现:
class Solution {
public int[] smallestK(int[] arr, int k) {
int[] ret = new int[k];
if (arr == null || k <= 0) {
return ret;
}
PriorityQueue<Integer> queue = new PriorityQueue<>();
for (int i = 0; i < arr.length; i++) {
queue.offer(arr[i]);
}
for (int i = 0; i < k; i++) {
ret[i] = queue.poll();
}
return ret;
}
}
二、堆(Heap)
1、背景知识:
JDK1.8中的PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。
2、概念:
堆就是一颗顺序存储的完全二叉树,底层是一个数组,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
3、堆的创建:
3.1、向下调整(以小根堆为例):
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?
3.2、思路如下:
- 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子);
- 如果parent的左孩子存在,即:child < size,进行以下操作,直到parent的左孩子不存在;
- parent右孩子是否存在,如果存在找到左右孩子中最小的孩子,让child进行标记;
- 将parent与较小的孩子child比较,如果parent小于较小的孩子child,调整结束;否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1;
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;
}
}
}
注意:在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下调整。
4、堆的插入:
堆的插入总共需要两个步骤:
- 先将元素放入到底层空间中(注意:空间不够时需要扩容)
- 将最后新插入的节点向上调整,直到满足堆的性质
//插入元素:
public void offer(int val){
//如果数组满了进行扩容;
if(isFull()){
elem=Arrays.copyOf(elem,2*elem.length);
}
elem[useSize]=val;
useSize++;
shiftUp(useSize-1);
}
//向上调整;
private void shiftUp(int child){
// 找到child的双亲
int parent=(child-1)/2;
while (child>0){
// 如果双亲比孩子小,交换两个元素,向上调整
if(elem[parent]<elem[child]){
int temp=elem[child];
elem[child]=elem[parent];
elem[parent]=temp;
child=parent;
parent=(child-1)/2;
}else {
break;
}
}
}
5、堆的删除:
注意:堆的删除一定删除的是堆顶元素。
思路如下:
- 将堆顶元素对堆中最后一个元素交换;
- 将堆中有效数据个数减少一个;
- 对堆顶元素进行向下调整;
//删除元素:
public void pop(){
//判断是否为空;
if(isEmpty()){
return;
}
//交换堆顶和最后一个元素;
swap(elem,0,useSize-1);
useSize--;
//向下调整;
shiftDown(0,useSize);
}
private void swap(int[] array,int i,int j)
{
int temp=elem[i];
elem[i]=elem[j];
elem[j]=temp;
}