【Java源码】集合类-优先队列PriorityQueue

1、类继承关系

public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable {

PriorityQueue只实现了AbstractQueue抽象类也就是实现了Queue接口。java

2、类属性

//默认初始化容量
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    
    //经过彻底二叉树(complete binary tree)实现的小顶堆
    transient Object[] queue; 

    private int size = 0;
    //比较器
    private final Comparator<? super E> comparator;
    //队列结构被改变的次数
    transient int modCount = 0;

根据transient Object[] queue; 的英文注释:node

Priority queue represented as a balanced binary heap: the two children of queue[n] are queue[2*n+1] and queue[2*(n+1)].数组

咱们能够知道PriorityQueue内部是经过彻底二叉树(complete binary tree)实现的小顶堆(注意:这里咱们定义的比较器为越小优先级越高)实现的。数据结构

3、数据结构

优先队列PriorityQueue内部是经过堆实现的。堆分为大顶堆和小顶堆:
多线程

  • 大顶堆:每一个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;
  • 小顶堆:每一个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
  • 堆经过数组来实现。
  • PriorityQueue内部是一颗彻底二叉树实现的小顶堆。父子节点下表有以下关系:
leftNo = parentNo*2+1

rightNo = parentNo*2+2

parentNo = (nodeNo-1)/2

经过上面的公式能够很轻松的根据某个节点计算出父节点和左右孩子节点。dom

4、经常使用方法

add()/offer()

其实add()方法内部也是调用了offer().下面是offer()的源码:ide

public boolean offer(E e) {
        //不容许空
        if (e == null)
            throw new NullPointerException();
        //modCount记录队列的结构变化次数。
        modCount++;
        int i = size;
        //判断
        if (i >= queue.length)
            //扩容
            grow(i + 1);
        //size加1
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            //不是第一次添加,调整树结构
            siftUp(i, e);
        return true;
    }

咱们能够知道:函数

  • add()和offer()是不容许空值的插入。
  • 和List同样,有fail-fast机制,会有modCount来记录队列的结构变化,在迭代和删除的时候校验,不经过会报ConcurrentModificationException。
  • 判断当前元素size大于等于queue数组的长度,进行扩容。若是queue的大小小于64扩容为原来的两倍再加2,反之扩容为原来的1.5倍。
    扩容函数源码以下:
private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        //若是queue的大小小于64扩容为原来的两倍再加2,反之扩容为原来的1.5倍
        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);
    }
  • 加入第一个元素时,queue[0] = e;之后加入元素内部数据结构会进行二叉树调整,维持最小堆的特性:调用siftUp(i, e):
private void siftUp(int k, E x) {
        //比较器非空状况
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }
    //比较器非空状况
    @SuppressWarnings("unchecked")
    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            //利用堆的特性:parentNo = (nodeNo-1)/2
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
    }

上面的源码中可知:线程

  1. 利用堆的特色:parentNo = (nodeNo-1)/2 计算出父节点的下标,由此获得父节点:queue[parent];
  2. 若是插入的元素x大于父节点e那么循环退出,不作结构调整,x就插入在队列尾:queue[k] = x;
  3. 不然 queue[k] = e; k = parent; 父节点和加入的位置元素互换,如此循环,以维持最小堆。

下面是画的是向一个优先队列中添加(offer)一个元素过程的草图,以便理解:
日志

poll(),获取并删除队列第一个元素

public E poll() {
        if (size == 0)
            return null;
        //
        int s = --size;
        modCount++;
        //获取队头元素
        E result = (E) queue[0];
        E x = (E) queue[s];
        //将最后一个元素置空
        queue[s] = null;
        if (s != 0)
            //若是不止一个元素,调整结构
            siftDown(0, x);
        //返回队头元素
        return result;
    }

删除元素,也得调整结构以维持优先队列内部数据结构为:堆

5、简单用法

下面是一段简单的事列代码,演示了天然排序和自定义排序的状况:

package com.good.good.study.queue;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Random;

/**
 * @author monkjavaer
 * @version V1.0
 * @date 2019/6/16 0016 10:22
 */
public class PriorityQueueTest {

    /**日志记录*/
    private static final Logger logger = LoggerFactory.getLogger(PriorityQueueTest.class);

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

    /**
     * 天然排序
     */
    private static void naturalOrdering(){
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        Random random = new Random();
        int size = 10;
        for (int i =0;i<size;i++){
            priorityQueue.add(random.nextInt(100));
        }
        for (int i =0;i<size;i++){
            logger.info("第 {} 次取出元素:{}",i,priorityQueue.poll());
        }
    }

    /**
     * 自定义排序规则,根据人的年龄排序
     */
    private static void personOrdering(){
        PriorityQueue<Person> priorityQueue = new PriorityQueue<>(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge()-o2.getAge();
            }
        });
        Random random = new Random();
        int size = 10;
        for (int i =0;i<size;i++){
            priorityQueue.add(new Person(random.nextInt(100)));
        }
        while (true){
            Person person = priorityQueue.poll();
            if (person == null){
                break;
            }
            logger.info("取出Person:{}",person.getAge());
        }
    }
}

6、应用场景

优先队列PriorityQueue经常使用于调度系统优先处理VIP用户的请求。多线程环境下咱们须要使用PriorityBlockingQueue来处理此种问题,以及须要考虑队列数据持久化。

相关文章
相关标签/搜索