优先级队列 :是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。
优先级队列相对于普通队列应该提供两个最基本的操作
(1)返回最高优先级对象(2)添加新的对象
在JDk1.8中的优先级队列底层使用了堆,而堆实际就是在完全二叉树的基础上进行了一些调整。
2.1堆的概念
堆这种数据结构本质上就是一个完全二叉树
并且堆中某个结点的值总是不大于或不小于其父结点的值
小堆:根节点最小的堆,满足Ki <= K2i+1 且 Ki <= K2i+2
大堆:根节点最大的堆, 满足Ki >= K2i+1 且 Ki >= K2i+2
堆的性质
2.2堆的存储方式
堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储
对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节 点,就会导致空间利用率比较低。
i为孩子节点,双亲节点为 (i - 1)/2
i为根节点,左孩子下标为2 * i + 1,右孩子下标为2 * i + 2
2.3堆的创建
2.3.1堆向下调整(以创建大堆为例)
//向下调整
private void shiftDown(int parent, int len) {
int child = 2 * parent + 1;
//最起码要有左孩子
while (child < len) {
//child+1
//获得左右孩子的最大值
if (child + 1 < len && elem[child] < elem[child + 1]) {
child++;
}
//child下标一定是左右孩子 最大值的下标
if (elem[child] > elem[parent]) {
int temp = elem[child];
elem[child] = elem[parent];
elem[parent] = temp;
//继续向下调整
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
然后通过循环每个结点,向下调整,然后创建好这棵树
public void createHeap() {
//usedSize-1表示最后一个child,(usedSize-1-1)/2表示最后一个父节点
for (int parent = (usedSize - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(parent, usedSize);
}
}
时间复杂度分析
第1层需要向下移动h-1层
第2层需要向下移动h-2层
…依次类推
分析过程如下:
这里以在大根堆前提下插入80为例:
代码如下:
//向上调整
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while (child > 0) {
if (elem[child] > elem[parent]) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
//插入元素 向上调整
public void offer(int val) {
if (isFull()) {
//扩容
elem = Arrays.copyOf(elem, 2 * elem.length);
}
elem[usedSize++] = val;//11
//向上调整
shiftUp(usedSize - 1);//10
}
public boolean isFull() {
return usedSize == elem.length;
}
需要判断堆中元素是否为空的情况
//删除元素
public void pop() {
if (isEmpty()) {
return;
}
swap(elem, 0, usedSize - 1);
usedSize--;
shiftDown(0, usedSize);
}
public void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
public boolean isEmpty() {
return usedSize == 0;
}
选择题练习
4.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是( C )
A: [3,2,5,7,4,6,8] B: [2,3,5,7,4,6,8]
C: [2,3,4,5,7,8,6] D: [2,3,4,5,6,7,8]
3.1PrinrityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线
程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
使用注意:
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());
}
}
此时创建出来的就是一个大堆。
描述:求前k个最大的数
适用情况: 在数据量比较大时,求数据集合中前K个最大的元素或者最小的元素
思路:
1)求前K个最大的元素,建立大小为K的小根堆
2)然后用剩下的集合里面的元素轮流和堆顶元素比较,如果剩下集合里面的元素比堆顶的元素大,那就替换掉堆顶的元素
3)然后向下调整,变成新的小根堆,此时这个堆中的元素就是前K个最大元素
代码如下:
public class Test {
//前k个最大的数
public static int[] maxK(int[] arr, int k) {
int[] ret = new int[k];
if (arr == null || k == 0) {
return ret;
}
Queue<Integer> minHeap = new PriorityQueue<>(k);
//总的时间复杂度:K * logK + (N-K) * logK = NlogK
//时间复杂度:K * logK
//1.遍历数组的前k个 放到堆当中
for (int i = 0; i < k; i++) {
minHeap.offer(arr[i]);
}
//2.遍历剩下的K-1个,每次和堆顶元素进行比较
//堆顶元素 小的时候,就出堆
//时间复杂度 : (N-K) * logK
for (int i = k; i < arr.length; i++) {
int val = minHeap.peek();
if (val < arr[i]) {
minHeap.poll();
minHeap.offer(arr[i]);
}
}
for (int i = 0; i < k; i++) {
ret[i] = minHeap.poll();
}
return ret;
}
public static void main(String[] args) {
int[] array = {1, 5, 43, 3, 2, 7, 98, 41, 567, 78};
int[] ret = maxK(array, 3);
System.out.println(Arrays.toString(ret));
}
}
如果要求前K个最小的元素,如何做?
和前面差不多,不同的是
(1)求前K个最小的元素,要建立大根堆
(2)比较的时候谁小,就把小的放在堆顶
面试题17.14.求前k个最小数-力扣(LeetCode)
class Solution {
public int[] smallestK(int[] arr, int k) {
int[] ret = new int[k];
if (arr == null || k == 0) {
return ret;
}
//PriorityQueue默认建立小根堆,但是这里我们要建立大根堆,就需要自己实现比较器
Queue<Integer> minHeap = new PriorityQueue<>(k,new Comparator<Integer>() {
@Override
//o1-o2就是小根堆
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
//总的时间复杂度:K * logK + (N-K) * logK = NlogK
//时间复杂度:K * logK
//1.遍历数组的前k个 放到堆当中
for (int i = 0; i < k; i++) {
minHeap.offer(arr[i]);
}
//2.遍历剩下的K-1个,每次和堆顶元素进行比较
//堆顶元素 大的时候,就出堆
//时间复杂度 : (N-K) * logK
for (int i = k; i < arr.length; i++) {
int val = minHeap.peek();
if (val > arr[i]) {
minHeap.poll();
minHeap.offer(arr[i]);
}
}
for (int i = 0; i < k; i++) {
ret[i] = minHeap.poll();
}
return ret;
}
}
4.1 PriorityQueue的实现
用堆作为底层结构封装优先级队列
4.2堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
这里要按照从小到大排序,就建立大根堆
public static void heapSort(int[] array) {
createBigHeap(array);
int end = array.length - 1;
while (end > 0) {
swap(array, 0, end);
shiftDown(array, 0, end);
end--;
}
}
private static void createBigHeap(int[] array) {
for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(array, parent, array.length);
}
}
private static void shiftDown(int[] array, int parent, int len) {
int child = parent * 2 + 1;
//至少有左孩子
while (child < len) {
if (child + 1 < len && array[child] < array[child + 1]) {
//有右孩子,且右孩子最大
child++;
}
if (array[child] > array[parent]) {
swap(array, child, parent);
parent = child;
child = 2 * parent + 1;
} else {
//child比parent小,不需要调整
break;
}
}
}
同样的,如果要从大到小排序,就要建立小根堆