目录
一、概念
队列是一种先进先出(FIFO)的数据结构,但是在某些情况下,操作可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该场景下,使用队列显然不合适,例如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。
二、PriorityQueue的介绍
2.1 关于PriorityQueue的使用要注意
import java.util.PriorityQueue;
2、PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常;
import java.util.PriorityQueue;
import java.util.Queue;
class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Test {
public static void main(String[] args) {
Queue<Student> heap = new PriorityQueue<>();
heap.offer(new Student("zhangsan", 25));
heap.offer(new Student("wangwu", 18));
}
}
3. 不能插入null对象,否则会抛出NullPointerException;
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
priorityQueue.offer(null);
4、没有容量限制,可以插入任意多个元素,其内部可以自动扩容;
5、插入和删除元素的时间复杂度为O(logN);
6. PriorityQueue底层使用了堆数据结;
2.2 PriorityQueue常用接口介绍
1、优先级队列的构造方法
三、优先级队列Top-K问题
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
为什么建小堆可以求出前k个最大元素呢?(求大根堆同理)
我们可以这样来理解,最开始我们拿数组的前k个元素建立成小堆,那么此时堆顶元素一定是前k个元素中的最小值数组,那此时我们剩下的元素与堆顶元素比较时,如果比堆顶元素还小,那么它一定不是前k个中的最大值,当数组元素大于堆顶元素时,这个值可能是要求的最大值,我们删除堆顶元素添加这个值,重新调整为小根堆,重复上述操作,最后小根堆里面就是我们要求的最大值.
class Solution {
public int[] smallestK(int[] arr, int k) {
if(k == 0) {
return new int[k];
}
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 {
int top = maxHeap.peek();
if(top > arr[i]) {
maxHeap.poll();
maxHeap.offer(arr[i]);
}
}
}
int[] ret = new int[k];
for (int i = 0; i < k; i++) {
int val = maxHeap.poll();
ret[i] = val;
}
return ret;
}
}
四、 模拟实现优先级队列
1、向下调整建立大根堆
public void createHeap() {
for (int parent = (usedSize - 1 - 1) / 2; parent >= 0; parent--) {
shfitDowm(parent, usedSize);
}
}
private void shfitDowm(int parent, int len) {
int child = parent * 2 + 1;
while (child < len) {
if(child + 1 < len && elem[child] < elem[child + 1]) {
child++;
}
//child的下标一定是最大值的下标
if(elem[child] > elem[parent]) {
swap(elem, parent, child);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
private void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
向下调整建立大根堆的时间复杂度:以满二叉树为例,第一层需向下调整h-1层,第二层需向下调整h-2层,第三层需向下调整h-3层……倒数第二层需向下调整1层,最后一层需向下调整0层
空间复杂度:没有申请额外的空间,故空间复杂度为O(1)
2、堆的插入
思想:插入元素的下一个位置,然后再向上调整,使整体成为一个大根堆。
public void offer(int val) {
if(isFull()) {
//扩容
this.elem = Arrays.copyOf(elem, elem.length * 2);
}
elem[usedSize++] = val;
shfitUp(usedSize - 1);
}
private void shfitUp(int child) {
int parent = (child - 1) / 2;
while (child > 0) {
if(elem[child] > elem[parent]) {
swap(elem, parent, child);
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
private void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
3、删除一个元素
思想:先让堆顶元素和最后一个元素进行交换,再将堆顶元素进行向下调整,使成为一个大根堆。
public void poll() {
if (isEmpty()) {
throw new EmptyException("没有元素啦!");
}
swap(elem,0,usedSize - 1);
usedSize--;
shfitDowm(0,usedSize);
}
private boolean isEmpty() {
return usedSize == 0;
}
4、堆排序
思想:大根堆堆顶元素一定是最大的,先将堆顶元素和最后一个元素交换,然后将堆顶元素进行向下调整,首先定义end = usedSize - 1,也就是计算最后一个元素的下标,使其与堆顶元素进行交换,此时该元素就到了堆顶,然后让该元素向下调整,一直进行循环,让其
end--,循环结束的条件为end > 0,因为只有一个元素时就不用进行向下调整了,本来就是有序的。
public void heapSort() {
int end = usedSize - 1;
while (end > 0) {
swap(elem, 0, end);
shfitDowm(0,end);
end--;
}
}
private void shfitDowm(int parent, int len) {
int child = parent * 2 + 1;
while (child < len) {
if(child + 1 < len && elem[child] < elem[child + 1]) {
child++;
}
//child的下标一定是最大值的下标
if(elem[child] > elem[parent]) {
swap(elem, parent, child);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
private void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}