文章目录
1.概念
前面学过队列,队列是一种先进先出的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列。
这种数据结构提供两个最基本的操作,返回最高优先级对象和添加新的对象(出队和入队)。
2.优先级队列的模拟实现
JDK1.8中的PriorityQueue底层使用了
堆
的数据结构,而堆实际就是在完全二叉树
的基础之上进行了一些元素的调整。
2.1 堆的概念
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式(分层遍历)存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。PriorityQueue默认是小根堆。
对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节
点,就会导致空间利用率比较低。
2.2堆的创建
2.2.1堆的向下调整
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆?
以大跟堆为例
思路:要想把这棵树变成大根堆,只需要将这棵树的每一棵子树都变成大根堆即可。
每一个绿框里都是一棵树,我们要把这些树都变成大根堆
private void shiftDown(int root,int len) {
int parent=root;
int child=parent*2+1;
while(child<len){
if(child+1<len&&elem[child]<elem[child+1]){
//child+1可能会越界
child++;
}
if(elem[child]>elem[parent]){
int tmp=elem[child];
elem[child]=elem[parent];
elem[parent]=tmp;
parent=child;
child=parent*2+1;
}else{
//如果根节点大于孩子结点就不用向下遍历比较,因为就是从最后一棵子树向上调整的
break;
}
}
}
向下调整的时间复杂度:O(log2(n))
n个结点树的高度是log2(n),就是向下调整的次数
数组下标从后往前遍历,然后每个下标向下调整
2.2.2堆的构建
public class PriorityQueue {
public int[] elem;
public int usedSize;
public static final int DEFAULT_CAPACITY=10;
public PriorityQueue() {
elem=new int[DEFAULT_CAPACITY];
}
/**
* 建堆的时间复杂度:O(n)
*
* @param array
*/
public void createHeap(int[] array) {
for(int i=0;i<array.length;i++){
elem[i]=array[i];
usedSize++;
}
for(int parent=(elem.length-1)/2;parent>=0;parent--){
shiftDown(parent,elem.length);
}
}
}
public class Test {
public static void main(String[] args) {
int[] array={ 27,15,19,18,28,34,65,49,25,37};
PriorityQueue priorityQueue=new PriorityQueue();
priorityQueue.createHeap(array);
}
}
建堆的时间复杂度:O(n)
2.2.3堆的插入
在数组最后位置插入一个数,(数组满了则要扩容)只需要向上调整,直到child=0
其他子树都是大根堆不需要调整
public void push(int val) {
if(isFull()){
//扩容
elem= Arrays.copyOf(this.elem,this.elem.length*2);
}
elem[usedSize]=val;
usedSize++;
shiftUp(usedSize-1);
}
private void shiftUp(int child) {
int parent=(child-1)/2;
while(child>0){
if(elem[parent]<elem[child]){
int tmp=elem[parent];
elem[parent]=elem[child];
elem[child]=tmp;
child=parent;
parent=(child-1)/2;
}else{
break;
}
}
}
public boolean isFull() {
return elem.length==usedSize;
}
插入后的结果:
2.2.4堆的删除
删除堆顶元素
将堆顶元素和最后一个元素交换,从堆顶开始向下调整
/**
* 出队【删除】:每次删除的都是优先级高的元素
* 仍然要保持是大根堆
*/
public void pollHeap() {
if(isEmpty()){
return;
}
int tmp=elem[0];
elem[0]=elem[elem.length-1];
elem[elem.length-1]=tmp;
usedSize--;
shiftDown(0,usedSize);
}
public boolean isEmpty() {
return usedSize==0;
}
结果:
2.3用堆模拟实现优先级队列
import java.util.Arrays;
public class PriorityQueue {
public int[] elem;
public int usedSize;
public static final int DEFAULT_CAPACITY=10;
public PriorityQueue() {
elem=new int[DEFAULT_CAPACITY];
}
/**
* 建堆的时间复杂度:O(n)
*
* @param array
*/
public void createHeap(int[] array) {
for(int i=0;i<array.length;i++){
elem[i]=array[i];
usedSize++;
}
for(int parent=(elem.length-1)/2;parent>=0;parent--){
shiftDown(parent,elem.length);
}
}
/**
*
* @param root 是每棵子树的根节点的下标
* @param len 是每棵子树调整结束的结束条件
* 向下调整的时间复杂度:O(logn)
*/
private void shiftDown(int root,int len) {
int parent=root;
int child=parent*2+1;
while(child<len){
if(child+1<len&&elem[child]<elem[child+1]){
child++;
}
if(elem[child]>elem[parent]){
int tmp=elem[child];
elem[child]=elem[parent];
elem[parent]=tmp;
parent=child;
child=parent*2+1;
}else{
break;
}
}
}
/**
* 入队:仍然要保持是大根堆
* @param val
*/
public void push(int val) {
if(isFull()){
//扩容
elem= Arrays.copyOf(this.elem,this.elem.length*2);
}
elem[usedSize]=val;
usedSize++;
shiftUp(usedSize-1);
}
private void shiftUp(int child) {
int parent=(child-1)/2;
while(child>0){
if(elem[parent]<elem[child]){
int tmp=elem[parent];
elem[parent]=elem[child];
elem[child]=tmp;
child=parent;
parent=(child-1)/2;
}else{
break;
}
}
}
public boolean isFull() {
return elem.length==usedSize;
}
/**
* 出队【删除】:每次删除的都是优先级高的元素
* 仍然要保持是大根堆
*/
public void pollHeap() {
if(isEmpty()){
return;
}
int tmp=elem[0];
elem[0]=elem[elem.length-1];
elem[elem.length-1]=tmp;
usedSize--;
shiftDown(0,usedSize);
}
public boolean isEmpty() {
return usedSize==0;
}
/**
* 获取堆顶元素
* @return
*/
public int peekHeap() {
if(isEmpty()){
return -1;
}
return elem[0];
}
}
3.常用接口介绍
PriorityQueue默认是小根堆。
PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
Student这个引用类型如果要进行比较,要实现Comparable<>接口,重写compareTo()方法
那么问题是它是什么时候在什么地方调用了我们重写的compareTo()方法?
3.1PriorityQueue集合类源码分析
分析PriorityQueue()构造方法:
PriorityQueue<Student> priorityQueue=new PriorityQueue<>();
对offer()分析:
如果想实现大根堆,只需要修改compareTo()方法
@Override
public int compareTo(Student o) {
return o.age-this.age;
}
那如果我想要Integer类型的元素变成大根堆?
Integer源码不能修改compareTo()方法
所以就用到comparator比较器
class IntCmp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
}
public class Test {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(new IntCmp());
priorityQueue.offer(10);
priorityQueue.offer(20);
priorityQueue.offer(3);
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
}
}
当没有传入数组容量时,默认容量是11,
当没有比较器时,必须是可比较的
优先使用比较器
//匿名内部类
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
}
//lambda表达式
PriorityQueue<Integer> priorityQueue2=new PriorityQueue<>((x,y)->{return x.compareTo(y);});
PriorityQueue<Integer> priorityQueue3=new PriorityQueue<>((x,y)->x.compareTo(y));
PriorityQueue是如何扩容的?
如果增加一个元素数组满了就扩容,根据原来数组的容量和64比较来决定是2倍扩还是1.5倍扩。扩容后新的容量如果大于整数最大值-8要进行调整,minCapacity是原来数组容量+1,如果minCapacity>整数最大值-8,就调整为整数最大值,否则就给整数最大值-8。
4.堆的应用
4.1 top-k问题最小的k个数
能想到的最简单直接的方式就是排序,但是如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决。
思路1
把arr数组所有元素建成小根堆,出队前k个元素,每出队1个,自动调整成小根堆,每次弹出的都是堆顶元素,自然是最小的。
时间复杂度:O(n+klogn)
建堆:O(n)
调整:O(klogn)
public static int[] smallestK2(int[] arr, int k) {
//1.建立一个小根堆
PriorityQueue<Integer> minHeap=new PriorityQueue<>();
//2.遍历数组,将数组的每个元素放到小根堆里
for (int i = 0; i < arr.length; i++) {
minHeap.offer(arr[i]);
}
//3.弹出k个元素,放到数组中,返回即可
int[] tmp=new int[k];
for (int i = 0; i < k; i++) {
tmp[i]=minHeap.poll();
}
return tmp;
}
如果想降低时间复杂度,下面给出第二种做法。
思路2
将前k个元素建成大根堆,遍历数组中剩下的元素,如果比堆顶元素小,则弹出堆顶元素,然后offer(),并调整为大根堆,遍历结束后堆里就是最小的k个数。
(和思路一的区别是没有整体建堆)
这里的k是很小的
时间复杂度:O(nlogk)
O(k+(n-k)logk)=O(k+nlogk-klogk)=O(nlogk)
class Solution {
public int[] smallestK(int[] arr, 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 val=maxHeap.peek();
if(arr[i]<val){
maxHeap.poll();
maxHeap.offer(arr[i]);
}
}
}
int[] tmp=new int[k];
for (int i = 0; i < k; i++) {
tmp[i]=maxHeap.poll();
}
return tmp;
}
}
这样写抛异常了,原因是没有判断k是否合法
正确写法:
class Solution {
public int[] smallestK(int[] arr, int k) {
if(arr==null||k==0){
return new int[0];
}
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 val=maxHeap.peek();
if(arr[i]<val){
maxHeap.poll();
maxHeap.offer(arr[i]);
}
}
}
int[] tmp=new int[k];
for (int i = 0; i < k; i++) {
tmp[i]=maxHeap.poll();
}
return tmp;
}
}
如果要求第k小的,就是大根堆堆顶元素
4.2堆排序
利用堆的思想来排序
升序:建大根堆
要想实现升序,如果建小根堆,然后分层弹出,还有另外设一个空间来存放这些弹出的数据,空间复杂度是O(n)。况且小根堆中的元素也不一定是升序的。
如果考虑建大根堆,让大根堆堆顶元素和最后一个交换,然后再向下调整成大根堆,然后再让大根堆堆顶元素和最后一个交换,并向下调整成大根堆…以此类推。
public class TestHeap {
public int[] elem;
public int usedSize;
public static final int DEFAULT_CAPACITY=10;
public TestHeap() {
elem=new int[DEFAULT_CAPACITY];
}
/**
* 建堆的时间复杂度:O(n)
*
* @param array
*/
public void createHeap(int[] array) {
for(int i=0;i<array.length;i++){
elem[i]=array[i];
usedSize++;
}
for(int parent=(usedSize-1-1)/2;parent>=0;parent--){
shiftDown(parent,usedSize);
}
}
}
private void shiftDown(int root,int len) {
int parent=root;
int child=parent*2+1;
while(child<len){
if(child+1<len&&elem[child]<elem[child+1]){
child++;
}
if(elem[child]>elem[parent]){
int tmp=elem[child];
elem[child]=elem[parent];
elem[parent]=tmp;
parent=child;
child=parent*2+1;
}else{
break;
}
}
}
public void heapSort(){
int end=usedSize-1;
while(end>0){
int tmp=elem[0];
elem[0]=elem[end];
elem[end]=tmp;
shiftDown(0,end);
end--;
}
}
public static void main5(String[] args) {
int[] array={ 27,15,19,18,28,34,65,49,25,37};
TestHeap testHeap=new TestHeap();
testHeap.createHeap(array);
testHeap.heapSort();
}
时间复杂度:O(n)+O(nlogn)约等于O(nlogn)
空间复杂度:O(1)