数据结构——优先级队列(堆)

一、优先级队列

1.1概念

有两个最基本操作,一是返回最高优先级对象,二是添加新对象,这种数据结构就叫做优先级队列。


二、优先级队列模拟实现

通过这种数据结构可以实现优先级队列,堆其实就是对完全二叉树进行了一些调整

2.1堆的概念

堆分为小根堆和大根堆,如果一颗完全二叉树,其左右节点均大于根结点,就叫做小根堆

如果左右节点均小于根结点,就叫做大根堆


2.2堆的性质

(1)堆总是一颗完全二叉树

(2)堆的某个节点总是大于或小于父亲结点的值。


2.3堆的存储方式

由于堆是一颗完全二叉树,因此我们可以用层序遍历的方式来实现高效存储。


2.4堆的创建 

2.4.1通过向下调整创建堆

调整过程(以大根堆为列):

(1)parent标记需要调整的节点,child标记parent的左孩子;

(2)如果parent的左孩子存在,即:child < size, 进行以下操作,直到parent的左孩子不存在

          [1] parent右孩子是否存在,存在找到左右孩子中最大的孩子,让child进行标记

          [2]将parent与较小的孩子child比较,如果parent大于较小的孩子child,调整结束,否则,交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = childchild = parent*2+1; 然后继续(2);

public class MyPriorityQueue {
     public int[] elem;
     public int usedSize;

     public MyPriorityQueue(){
         this.elem=new int[10];
     }

     //传一个数组,对堆初始化
     public void initElem(int[] array){
         for(int i=0;i<array.length;i++){
             this.elem[i]=array[i];
             //每插入一个数据,usedSize++
             this.usedSize++;
         }
     }

     public void createPriorityQueue( ){
         //计算出最后一颗子树的父亲节点并用parent标记,然后
         //通过parent--对每棵子树进行调整,直到parent<0
         for (int parent=(this.usedSize-1-1)/2;parent>=0;
         parent--) {
             //向下调整
             shiftDown(parent,this.usedSize);
         }
     }

     //向下调整
    private void shiftDown(int parent, int usedSize) {
         int child=2*parent+1;
         while(child<usedSize){
             //找到左右孩子最大值
             if(child+1<usedSize && 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=2*parent+1;
             }else{
                 //说明当前树已经调整完了
                 break;
             }
         }
    }

2.4.2建堆的时间复杂度

建堆的时间复杂度为O(N)


2.5堆的插入与删除

2.5.1堆的插入
 public void offer(int val){
         if(isFull()){
             elem= Arrays.copyOf(elem,
                     2*elem.length);
         }
         elem[usedSize]=val;
         int child=usedSize;
         int parent=(child-1)/2;
         usedSize++;
         while(parent>=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;
             }
         }

    }

图示如下(图中是以小根堆为例)


2.5.2堆的删除
public int poll(){
         //先判断队列是否为空
         if(isEmpty()){
             return -1;
         }
         int val=elem[0];
         swap(Collections.singletonList(elem),
                 0,usedSize-1);
         shiftDown(0,usedSize-1);
         usedSize--;
         return val;
    }
    public boolean isEmpty(){
         return usedSize==0;
    }

 图示:


 2.6常用接口

2.6.1PriorityQueue的特性
Java 集合框架中提供了 PriorityQueue PriorityBlockingQueue 两种类型的优先级队列 PriorityQueue 是线 程不安全的, PriorityBlockingQueue 是线程安全的。
关于 PriorityQueue 的使用要注意:
1. 使用时必须导入 PriorityQueue 所在的包
2. PriorityQueue 中放置的 元素必须要 能够比较大小 ,不能插入无法比较大小的对象,否则会抛出
ClassCastException 异常
3. 不能插入null对象 ,否则会抛出 NullPointerException
4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
5. 插入和删除元素的时间复杂度为O(logN)
6. PriorityQueue 底层使用了堆数据结构
7. PriorityQueue 默认情况下是小堆 --- 即每次获取到的元素都是最小的元素

2.6.2PriorityQueue常用接口介绍
2.6.2.1优先级队列的构造

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());
    }
}

 


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

 

static void TestPriorityQueue2(){
        int[] arr = {4,1,9,2,8,0,7,3,6,5};
// 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
// 否则在插入时需要不多的扩容
// 扩容机制:开辟更大的空间,拷贝元素,这样效率会比较低
        PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
        for (int e: arr) {
            q.offer(e);
        }
        System.out.println(q.size()); // 打印优先级队列中有效元素个数
        System.out.println(q.peek()); // 获取优先级最高的元素
// 从优先级队列中删除两个元素之和,再次获取优先级最高的元素
        q.poll();
        q.poll();
        System.out.println(q.size()); // 打印优先级队列中有效元素个数
        System.out.println(q.peek()); // 获取优先级最高的元素
        q.offer(0);
        System.out.println(q.peek()); // 获取优先级最高的元素
// 将优先级队列中的有效元素删除掉,检测其是否为空
        q.clear();
        if(q.isEmpty()){
            System.out.println("优先级队列已经为空!!!");
        }
        else{
            System.out.println("优先级队列不为空");
        }
    }

 注意:以下是JDK 1.8中,PriorityQueue的扩容方式:

  private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private void grow(int minCapacity) {
        int oldCapacity = queue.length;
// Double size if small; else grow by 50%
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                (oldCapacity + 2) :
                (oldCapacity >> 1));
// overflow-conscious code
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        queue = Arrays.copyOf(queue, newCapacity);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            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 来进行扩容

2.7堆的应用

2.7.1堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
序:建 堆(即从小到大)
序:建 堆(即从大到小)
2. 利用 堆删除思想 来进行排序
图示:( 以升序为例,降序思路相同

 代码示例:

public void headSort(){
         int end=usedSize-1;
         while(end>0){
             swap(Collections.singletonList(elem),0,end);
             shiftDown(0,end);
             end--;
         }
    }

2.7.2Top-K问题 
TOP-K 问题:即求数据集合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前 10 名、世界 500 强、富豪榜、游戏中前 100 的活跃玩家等。
对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是: 如果数据量非常大,排序就不太可取了 ( 可能数据都不能一下子全部加载到内存中) 。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
k 个最 的元素,则建
k 个最 的元素,则建
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。
图示:

 

 

  • 27
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值