PriorityQueue优先级队列

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)

猜你喜欢

转载自blog.csdn.net/qq_63983125/article/details/127262471