优先级队列及堆

本文详细介绍了优先级队列的概念、接口和应用,指出PriorityQueue在Java中的实现基于堆数据结构。堆是一种特殊的树形数据结构,用于高效地维护有序集合。通过向下调整和向上调整操作,可以实现堆的创建和维护。堆排序和Top-k问题是堆的重要应用场景。此外,文章还探讨了自定义比较器以实现大堆和小堆的转换。
摘要由CSDN通过智能技术生成

1. 优先级队列

1.1 概念

队列是一种先进先出的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素出队列。该场景中,使用队列显然不合适,比如:在手机上玩游戏时,如果有电话打进来,那么系统应该先处理队列。
在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列

1.2 常用接口介绍

1.2.1 PriorityQueue的特性

关于PriorityQueue的使用要注意:

  1. 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
  1. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastExpection异常。
  2. 不能插入null对象,否则会抛出NullPointerExpection。
  3. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容。
  4. 插入和删除元素的时间复杂度为 O ( l o g 2 N ) O(log_2N) O(log2N).
  5. PriorityQueue底层使用了堆数据结构,(后面介绍)。
  6. PriorityQueue默认情况下是小堆-----即每次获取到的元素都是最小的元素。

1.2.2 PriorityQueue常用接口介绍

  1. 优先级队列的构造
    此处只列出了几种构造方式,其他的方式可以参考帮助文档。
构造器功能介绍
PriorityQueue()创建一个空的优先级队列
PriorityQueue(int initialCapacity)创建一个初始容量为initialCapacity的优先级队列。注意:initialCapacity不能小于1,否则会抛出ILLegalArgumentException异常
PriorityQueue(Collection<?extends E>c)用一个集合来创建优先级队列
public class Priority {
    static void TestPriorityQueue(){
        //创建一个空的优先级队列
        PriorityQueue<Integer> p1=new PriorityQueue<>();//默认容量是11
        //创建一个空的优先级队列,底层的容量为initialCapacity
        PriorityQueue<Integer> p2=new PriorityQueue<>(100);

        ArrayList<Integer> list=new ArrayList<>();
        list.add(4);
        list.add(3);
        list.add(2);
        list.add(1);
        PriorityQueue<Integer> p3=new PriorityQueue<>(list);
        System.out.println(p3.size());
        System.out.println(p3.peek());
    }

    public static void main(String[] args) {
        TestPriorityQueue();
    }
}

注意:默认情况下,PriorityQueue队列是小堆,如果需要大堆,需要用户提供比较器。

  • 用户自己提供比较器
public class Card {
    String rank;
    String suit;

    public Card(String rank,String suit){
        this.rank=rank;
        this.suit=suit;
    }
}
public class CardCmp implements Comparator<Card>{
    @Override
    public int compare(Card o1, Card o2) {
        o1.rank.compareTo(o2.rank) ;
        return 0;
    }
}

验证:PriorityQueue中放置的元素必须能够比较大小,否则就会抛出ClassCastExpection

public static void method3(){
        PriorityQueue<Card> p=new PriorityQueue<>(new CardCmp());
        p.offer(new Card("A","♠"));
        p.offer(new Card("K","♠"));
    }
  • 默认情况下是小堆,如何创建大堆呢?----起始元素是最大的
public static void method4(){
     PriorityQueue<Integer> p=new PriorityQueue<>(new Comparator<Integer>() {
         @Override
         public int compare(Integer o1, Integer o2) {
             return o2-o1;//o2-o1是大堆, o1-o2是小堆
         }
     });
        p.offer(5);
        p.offer(1);
        p.offer(4);
        p.offer(2);
        p.offer(3);
    }

插入/删除/获取优先级最高的元素

函数名功能介绍
boolean offer(E e)插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 O ( l o g 2 N ) O(log_2N) O(log2N),注意:空间不够时会进行扩容
E peek()获取优先级最高的元素,如果优先级队列为空,返回null
E poll()移除优先级最高的元素并返回,如果优先级队列为空,返回null
int size()获取有效元素的个数
void clear()清空
boolean isEmpty()检测优先级队列是否为空,空的话返回true
public static void method2(){
        PriorityQueue<Integer> p=new PriorityQueue<>();
        p.offer(5);
        p.offer(1);
        p.offer(4);
        p.offer(2);
        p.offer(3);
        System.out.println(p.size());

        p.offer(null);//空指针异常
        System.out.println(p.peek());//获取堆顶元素----即优先级最大或者最小的元素
        p.poll();
        p.poll();
        p.poll();
        System.out.println(p.peek());

        p.clear();
        if(p.isEmpty()){
            System.out.println("p is empty");
        }else{
            System.out.println("p is not empty");
        }
    }

下面是PriorityQueue的扩容方式:

public class Test {
    private static final int MAX_ARRAY_SIZE=Integer.MAX_VALUE-8;

    private void grow(int minCapacity){
        int oldCapacity= queue.length;
        int newCapacity=oldCapacity+((oldCapacity<64)?(oldCapacity+2):(oldCapacity>>1));
        if(newCapacity-MAX_ARRAY_SIZE>0){
            newCapacity=hugeCapacity(minCapacity);
            queue= Arrays.copyOf(queue,newCapacity);
        }
    }
    private static int hugeCapacity(int minCapacity){
        if(minCapacity<0){
            throw new OutOfMemoryError();
            return (minCapacity>MAX_ARRAY_SIZE)?Integer.MAX_VALUE:MAX_ARRAY_SIZE;
        }
    }
}

优先级队列的扩容说明

  • 如果容量小于64时,是按照oldCapacity的2倍方式扩容的。
  • 如果容量大于64时,是按照oldCapacity的1.5倍方式扩容的。
  • 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容。

1.3 优先级队列的应用

top-k:最大或者最小的前k个数据。
top-k问题:最小的k个数

class Solution {
    public int[] smallestK(int[] arr, int k) {
        if(arr==null){
            return new int[0];
        }

        //用数组中的所有元素构造一个小堆
        PriorityQueue<Integer> p=new PriorityQueue<>();
        for(int i=0;i<arr.length;++i){
            p.offer(arr[i]);
        }

        //获取堆中前k个元素
        int[] ret =new int[k];
        for(int i=0;i<k;++i){
            ret[i]=p.poll();
        }

        return ret;
    }
}

2. 优先级队列的模拟实现

PriorityQueue底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调整。

2.1 堆的概念

如果有一个关键码的集合K={ k 0 , k 1 , k − 2 , . . . , k n − 1 k_0,k_1,k-2,...,k_n-1 k0k1k2...kn1},把它的所有元素按完全二叉树的顺序存储方式存储。在一个一维数组中,并满足:Ki<=K2i+2(Ki>=K2i+2)i=0,1,2…,则称为小堆 (或大堆)。将根结点最大的堆叫最大堆大根堆,根结点最小的堆叫做最小堆小根堆
堆的性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆中总是一棵完全二叉树;

2.2 堆的存储方式

从堆的概念可知,堆是一棵完全二叉树,因此可以用层序的规则采用顺序的方式来高校存储。
注意:对于非完全二叉树,不适合使用顺序方式进行存储。
原因:为了能够还原二叉树,空间中必须要存储空结点,就会导致空间利用率比较低。
将元素存储到数组中后,可以根据二叉树部分的性质5对树进行还原。假设i为结点在数组中的下标,则有:

  • 如果i为0,则i表示的结点为根结点,否则i结点的双亲结点为(i-1)/2
  • 如果2i+1小于结点个数,则结点i的左孩子下标为2i+1,否则没有左孩子
  • 如果2i+2小于结点个数,则结点i的右孩子下标为2i+2,否则没有右孩子

2.3 堆的创建

2.3.1 堆向下调整

对于集合{27,15,19,18,28,34,65,49,25,37}中的数据,如何将其创建成堆呢?
在这里插入图片描述
观察上图发现:根结点的左右子树已经完全满足堆的性质,因此只需要将根结点向下调整即可。
向下调整过程

  • 让parent标记需要调整的结点,child标记parent的左孩子。
  • 如果parent的左孩子存在,即child<size,则进行以下操作,知道parent的左孩子不存在
  • parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child标记
  • 将parent和child比较,如果parent比child大,则交换两个,parent中的大元素向下移动,可能导致子树不满足性质,因此需要继续向下调整
// 功能:调整以parent为根的二叉树
    //    前提:必须要保证parent的左右子树已经满足堆的特性
    // 时间复杂度:O(logN)
    private void shiftDown(int parent){
        // 默认让child先标记左孩子---因为:parent可能有左没有右
        int child = parent*2 + 1;

        // while循环条件可以保证:parent的左孩子一定存在
        //       但是不能保证parent的右孩子是否存在
        while(child < size){
            // 1. 找到左右孩子中较小的孩子
            if(child+1 < size && array[child+1] < array[child]){
                child += 1;
            }

            // 2. 较小的孩子已经找到了
            //    检测双亲和孩子间是否满足堆的特性
            if(array[parent] > array[child]){
                swap(parent, child);

                // 大的双亲往下走了,可能会导致子树又不满足堆的特性
                // 因此需要继续往下调整
                parent = child;
                child = parent*2 + 1;
            }else{
                // 以parent为根的二叉树已经是堆了
                return;
            }
        }
    }

注意:在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下调整。
时间复杂度:从根结点一直比到叶子结点,比较的次数为完全二叉树的高度,即 O ( l o g 2 N ) O(log_2N) O(log2N)

2.3.2 堆的创建

那么对于普通的序列{1,5,3,8,7,6},即根结点的左右子树不满足堆的特性,又该如何调整呢?

public MyPriorityQueue(Integer[] arr){
        // 1. 将arr中的元素拷贝到数组中
        array = new Integer[arr.length];
        for(int i = 0; i < arr.length; ++i){
            array[i] = arr[i];
        }
        size = arr.length;

        // 2. 找当前完全二叉树中倒数第一个叶子节点
        //    注意:倒数第一个叶子节点刚好是最后一个节点的双亲
        //    最后一个节点的编号size-1  倒数第一个非叶子节点的下标为(size-1-1)/2
        int lastLeafParent = (size-2)/2;

        // 3. 从倒数第一个叶子节点位置开始,一直到根节点的位置,使用向下调整
        for(int root = lastLeafParent; root >= 0; root--){
            shiftDown(root);
        }
    }

2.3.3 建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明。
假设树的高度为h:
在这里插入图片描述
注意:叶子结点不需要调整,因为是从倒数第一个非叶子结点开始调整的。
第1层, 2 0 2^0 20个结点,需要向下移动h-1层;
第2层, 2 1 2^1 21个结点,需要向下移动h-2层;
第3层, 2 2 2^2 22个结点,需要向下移动h-3层;
第4层, 2 3 2^3 23个结点,需要向下移动h-4层;

第h-1层, 2 h − 2 2^{h-2} 2h2个结点,需要向下移动1层;
则需要移动结点总的移动步数:
T ( n ) = 2 0 ∗ ( h − 1 ) + 2 1 ∗ ( h − 2 ) + 2 2 ∗ ( h − 3 ) + 2 3 ∗ ( h − 4 ) + . . . + 2 h − 3 ∗ ( 2 ) + 2 h − 2 ∗ ( 1 ) T(n)=2^0*(h-1)+2^1*(h-2)+2^2*(h-3)+2^3*(h-4)+...+2^{h-3}*(2)+2^{h-2}*(1) T(n)=20(h1)+21(h2)+22(h3)+23(h4)+...+2h3(2)+2h2(1) ------------ ①
2 T ( n ) = 2 1 ∗ ( h − 1 ) + 2 2 ∗ ( h − 2 ) + 2 3 ∗ ( h − 3 ) + 2 4 ∗ ( h − 4 ) + . . . + 2 h − 2 ∗ ( 2 ) + 2 h − 1 ∗ ( 1 ) 2T(n)=2^1*(h-1)+2^2*(h-2)+2^3*(h-3)+2^4*(h-4)+...+2^{h-2}*(2)+2^{h-1}*(1) 2T(n)=21(h1)+22(h2)+23(h3)+24(h4)+...+2h2(2)+2h1(1) -------------②
②-①错位相减:
T ( n ) = 1 − h + 2 1 + 2 2 + 2 3 + 2 4 + . . . + 2 h − 2 + 2 h − 1 T(n)=1-h+2^1+2^2+2^3+2^4+...+2^{h-2}+2^{h-1} T(n)=1h+21+22+23+24+...+2h2+2h1
   = 2 0 + 2 1 + 2 2 + 2 3 + 2 4 + . . . + 2 h − 2 + 2 h − 1 − h =2^0+2^1+2^2+2^3+2^4+...+2^{h-2}+2^{h-1}-h =20+21+22+23+24+...+2h2+2h1h
   = 2 h − 1 − h =2^h-1-h =2h1h
又因为n= 2 h − 1 2^h-1 2h1 h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1)
T ( n ) = n − l o g 2 ( n + 1 ) ≈ n T(n)=n-log_2(n+1)≈n T(n)=nlog2(n+1)n

2.4 堆的插入与删除

2.4.1 堆的插入

堆的插入总共需要两个步骤:

  1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
  2. 将最后新插入的结点向上调整,直到满足堆的性质
private void shiftUp(int child){
//找到child的双亲
int parent = (child-1)/2;
 while(child != 0){
        if(array[child] < array[parent]){
            swap(child, parent);
        child = parent;
        parent = (child-1)/2;
        }else{
        return;
        }
    }
}

2.4.2 堆的删除

注意:堆的删除一定是堆顶元素。

  1. 将堆顶元素与最后一个元素交换
  2. 将堆中有效元素个数减少一个
  3. 将堆顶元素向下调整

2.5 用堆模拟实现优先级队列

public class MyPriorityQueue {
    Integer[] array;
    int size;   // 有效元素的个数
boolean offer(Integer e){
        if(e == null){
            throw new NullPointerException("插入时候元素为null");
        }

        ensureCapacity();

        array[size++] = e;

        // 注意:当新元素插入之后,可能会破坏堆的性质---需要向上调整
        shiftUp(size-1);
        return true;
    }
     // 将堆顶的元素删除掉
    public Integer poll(){
        if(isEmpty()){
            return null;
        }

        Integer ret = array[0];

        // 1. 将堆顶元素与堆中最后一个元素交换
        swap(0, size-1);

        // 2. 将堆中有效元素个数减少一个
        size--;  // size -= 1;

        // 3. 将堆顶元素往下调整到合适位置
        shiftDown(0);
        return ret;
    }
public int size(){
        return size;
    }

    public boolean isEmpty(){
        return size == 0;
    }

    public void clear(){
        size = 0;
    }
    public int peek(){
    return array[0];
    }
}

3. 堆的应用

3.1 PriorityQueue的实现

用堆作为底层结构封装优先级队列

3.2 堆排序

堆排序即用堆的思想来进行排序,总共分为两个步骤:

  1. 建堆
    升序:建大堆
    降序:建小堆
  2. 利用堆删除思想来进行排序
public static void swap(int [] array,int left,int right){
        int temp=array[right];
        array[right]=array[left];
        array[left]=array[right];

    }
    public static void shiftDown(int[] array,int size, int parent){
        int child =parent*2+1;

        while(child<size){
            //找左右孩子中较大的孩子
            if(child+1<size&&array[child+1]>array[child]){
                child+=1;
            }

            //双亲小于较大的孩子
            if(array[parent]<array[child]){
                swap(array,parent,child);
            }
        }
    }

    //假设:升序
    public static void heapSort(int[] array){
        //1.建堆---升序 建大堆   降序 建小堆
        for(int root=(array.length-2)>>1;root>=0;root--){
            shiftDown(array,array.length,root);
        }

        //2.利用堆删除的思想来排序---向下调整
        int end=array.length-1;//用end标记最后一个元素
        while(end!=0){
            swap(array,0,end);
            shiftDown(array,end,0);
            end--;
        }
    }

3.3 Top-k问题

Top-k问题:求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
对于top-k问题,能想到的最简单的方式就是排序,但是如果数据量非常大,排序就不太可取了。最佳的方式是用堆来解决。基本思路如下:

  1. 用数据集合中前K个元素来建堆
    前K个最大的元素则建小堆
    前K个最小的元素则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
    将剩余N-K个元素依次与堆顶元素比较之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值