优先级队列之PriorityQueue源码分析

优先级队列之PriorityQueue

上一篇文章笔主介绍了BlockingQueue的实现类之一,即DelayQueue。了解到DelayQueue对于元素的存储是完全借助于PriorityQueue来实现的,今天,我们来学习下优先级队列之PriorityQueue

PriorityQueue的定义

我们看下JDK中对的PriorityQueue定义,如下:

An unbounded priority {@linkplain Queue queue} based on a priority heap. The elements of the priority queue are ordered according to their {@linkplain Comparable natural ordering}, or by a {@link Comparator} provided at queue construction time, depending on which constructor is used. A priority queue does not permit {@code null} elements. A priority queue relying on natural ordering also does not permit insertion of non-comparable objects (doing so may result in {@code ClassCastException}).

PriorityQueue是基于优先级堆实现的无界优先级队列。优先级队列中的元素根据其可比较的自然顺序来排序,或者由构造队列时提供的比较器来排序,这取决于使用的是哪个构造函数。优先级队列不允许插入空元素。一个依赖自然排序的优先级队列同样不允许插入不可比较的对象(这样做可能导致ClassCastException)。

The head of this queue is the least element with respect to the specified ordering. If multiple elements are tied for least value, the head is one of those elements – ties are broken arbitrarily. The queue retrieval operations {@code poll}, {@code remove}, {@code peek}, and {@code element} access the element at the head of the queue.

队列的头部是与指定顺序相关的最小元素。如果有多个相同优先级的最小元素,则从中任意选取一个作为队列的头。队列的检索操作如poll()、remove()、peek()、element()都是从队列的头部元素开始。

A priority queue is unbounded, but has an internal capacity governing the size of an array used to store the elements on the queue. It is always at least as large as the queue size. As elements are added to a priority queue, its capacity grows automatically. The details of the growth policy are not specified.

优先级队列是无界的,但队列具有可以控制存储元素数组大小的内部容量。它总是至少和队列大小一样大。当元素被添加到优先级队列时,其容量会自动增长。队列容量的增长策略细节并没有指定。

Note that this implementation is not synchronized. Multiple threads should not access a {@code PriorityQueue} instance concurrently if any of the threads modifies the queue. Instead, use the thread-safe java.util.concurrent.PriorityBlockingQueue} class.

需要注意的是,优先级队列并不是同步的。当一个线程正在修改队列时,其他多个线程不应该同时访问队列。可以使用线程安全的PriorityBlockingQueue。

PriorityQueue的特点

简单总结下,PriorityQueue有以下特点:

  • PriorityQueue是一个无界队列,没有容量限制。
  • PriorityQueue不允许插入空元素,如果提供了比较器,插入的元素就按照比较器排序。否则,按照自然顺序来排序。
  • PriorityQueue内部没有实现同步,是线程不安全的队列。

接下来,我们从源码的角度来认识下PriorityQueue。

PriorityQueue类的成员属性

PriorityQueue主要定义了以下成员属性,源码如下:

    //队列的默认初始容量
	private static final int DEFAULT_INITIAL_CAPACITY = 11;

   /**
    * 优先级队列内部使用数组来存储元素
    * 实际上,此数组使用了堆排序的方式构建小根堆,使队列的头部总是最小的元素
    **/
    transient Object[] queue; 
    
    //队列中的元素个数
    private int size = 0;

    //元素排序使用的比较器,如果comparator为null,则使用自然排序
    private final Comparator<? super E> comparator;

    //记录队列结构修改的次数,当队列中有元素插入、移除或者删除时,队列的结构会发生变化
    transient int modCount = 0; 
   
   //数组的最大长度
   private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
PriorityQueue类的构造方法

PriorityQueue提供了六种方式来构造阻塞队列,分别是:

  • 默认无参构造方法,既不指定队列的初始容量,也不指定比较器。
  • 指定初始容量的构造方法
  • 指定比较器的构造方法
  • 指定初始容量和比较器的构造方法
  • 使用给定集合构造队列的方法
  • 使用给定优先级队列构造队列的方法
  • 使用给定SortedSet构造队列的方法

构造方法的源码如下:

    //默认无参构造方法
    public PriorityQueue() {
        //使用默认初始容量,指定比较器为nul,插入的元素使用自然排序
        this(DEFAULT_INITIAL_CAPACITY, null);
    }

    //使用指定初始容量的构造方法
    public PriorityQueue(int initialCapacity) {
        //指定比较器为nul,插入的元素使用自然排序
        this(initialCapacity, null);
    }

    //使用指定比较器的构造方法   
    public PriorityQueue(Comparator<? super E> comparator) {
         //使用指定的比较器和默认初始容量
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }

    //使用指定初始容量和比较器的构造方法   
    public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }

  
    //使用给定集合构造队列的方法
    public PriorityQueue(Collection<? extends E> c) {
        if (c instanceof SortedSet<?>) {  //c是SortedSet的实例
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
            //从集合中初始化队列元素
            initElementsFromCollection(ss);
        }
        else if (c instanceof PriorityQueue<?>) { //c是PriorityQueue的实例
            PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            //从队列中初始化队列元素
            initFromPriorityQueue(pq);
        }
        else {
            this.comparator = null;
            initFromCollection(c);
        }
    }

   
    //使用给定优先级队列构造队列的方法
    public PriorityQueue(PriorityQueue<? extends E> c) {
        this.comparator = (Comparator<? super E>) c.comparator();
        initFromPriorityQueue(c);
    }

  
    //使用给定SortedSet构造队列的方法
    public PriorityQueue(SortedSet<? extends E> c) {
        this.comparator = (Comparator<? super E>) c.comparator();
        initElementsFromCollection(c);
    }
PriorityQueue类的成员方法

PriorityQueue类的成员方法可以划分为以下几类:

  • 公共方法
  • 插入方法
  • 移除方法

这里公共方法是指在类内部会被多次调用的方法或者是获取队列属性的方法,例如:扩大数组容量的方法、根据给定数组下标删除队列元素的方法、获取队列元素个数的方法、清空整个队列的方法、将队列转换为数组的方法等等。源码如下:

    //使用给定优先级队列构造队列
    private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
        if (c.getClass() == PriorityQueue.class) {  //如果c是优先级队列
            this.queue = c.toArray();  //数组直接赋值
            this.size = c.size();           //队列大小直接赋值
        } else {
            initFromCollection(c); //使用给定集合构造队列
        }
    }

    //使用给定集合中的元素初始化一个优先级队列
    private void initElementsFromCollection(Collection<? extends E> c) {
        Object[] a = c.toArray();
        // 如果集合c转化为数组不是Object数组,就将其转化为Object数组
        if (a.getClass() != Object[].class)
            a = Arrays.copyOf(a, a.length, Object[].class);
        int len = a.length;
        if (len == 1 || this.comparator != null)
            for (int i = 0; i < len; i++)  //检查数组中是否有null元素
                if (a[i] == null) 
                    throw new NullPointerException();
        this.queue = a;  //数组直接赋值
        this.size = a.length;  //队列大小直接赋值
    }

    //使用给定集合构造队列
    private void initFromCollection(Collection<? extends E> c) {
        initElementsFromCollection(c);
        //向小调整构建最小堆
        heapify();
    }
    
    //扩大数组容量
	private void grow(int minCapacity) {
	    //获取当前的队列容量
        int oldCapacity = queue.length;
        /**
         * 如果当前容量小于64,新的容量等于当前容量的2倍再加2。
         * 否则,新的容量等于当前容量的1.5倍。
         **/
        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) {
        //如果minCapacity小于0,说明minCapacity超过了Integer能表示的最大范围,已经溢出
        if (minCapacity < 0) // overflow
            //容量溢出就抛出OOM
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
 
    //如果队列为空,返回null。否则,返回队列头部元素。
    @SuppressWarnings("unchecked")
    public E peek() {
        return (size == 0) ? null : (E) queue[0];
    }
   
    //根据给定元素确定其在队列中的位置
    private int indexOf(Object o) {
        if (o != null) {
            for (int i = 0; i < size; i++)
                if (o.equals(queue[i]))  //找到返回对应的下标
                    return i;
        }
        return -1;  //未找到返回-1
   }
  
   //判断队列中是否包含给定的元素o
   public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

    //将队列转换为Object数组
    public Object[] toArray() {
        return Arrays.copyOf(queue, size);
    }

    //将队列转换为T类型数组
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        final int size = this.size;
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(queue, size, a.getClass());
        System.arraycopy(queue, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

    //返回队列的迭代器对象
    public Iterator<E> iterator() {
        return new Itr();
    }

    //返回队列中的元素个数
    public int size() {
        return size;
    }

    //清空整个队列
   public void clear() {
        modCount++;
        for (int i = 0; i < size; i++)
            queue[i] = null;
        size = 0;
   }

  //根据给定的下标删除元素
  private E removeAt(int i) {
        modCount++;
        int s = --size;
        if (s == i) // removed last element
             //如果删除的是最后一个元素,直接将数组最后一个元素置为null
            queue[i] = null;
        else {
            E moved = (E) queue[s];
            queue[s] = null;
            //向下调整构造最小堆
            siftDown(i, moved);
            //queue[i] == moved说明数组最后一个元素直接移动到了被删除元素的位置
            if (queue[i] == moved) {
                //此时需要向上调整构造最小堆
                siftUp(i, moved);
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
    }

    //向上调整构造最小堆
    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

    //使用自然排序向上调整构造最小堆
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            //计算第k个节点的父节点的下标
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            //如果x大于第k个节点的父节点,说明已是最小堆,直接跳出循环
            if (key.compareTo((E) e) >= 0)
                break;
            //交换第k个节点与父节点的元素
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

    //使用比较器向上调整构造最小堆
    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
           //计算第k个节点的父节点的下标
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            //如果x大于第k个节点的父节点,说明已是最小堆,直接跳出循环
            if (comparator.compare(x, (E) e) >= 0)
                break;
            //交换第k个节点与父节点的元素
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
    }

    //向下调整构造最小堆
    private void siftDown(int k, E x) {
        if (comparator != null)
             //比较器不为null,使用比较器构造最小堆
            siftDownUsingComparator(k, x);
        else
            //比较器为null使用自然排序构造最小堆
            siftDownComparable(k, x);
    }

    //使用自然排序向下调整构造最小堆
    private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1;        // loop while a non-leaf
        while (k < half) {
             //计算第k个节点的左孩子节点下标,第k个节点的左孩子节点为2k+1
            int child = (k << 1) + 1; // assume left child is least
            Object c = queue[child];
            //计算第k个节点的右孩子节点下标,第k个节点的右孩子节点为2k+2
            int right = child + 1;
            //获取左右孩子中最小的节点
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            //如果key小于c,说明已经是最小堆,直接跳出循环
            if (key.compareTo((E) c) <= 0)
                break;
            //将左右孩子中最小的节点放在第k个节点位置
            queue[k] = c;
            k = child; //此处k重新赋值
        }
        //将元素x放在第k个节点位置
        queue[k] = key;
    }

    //使用比较器排序向下调整构造最小堆
    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
             //计算第k个节点的左孩子节点下标,第k个节点的左孩子节点为2k+1
            int child = (k << 1) + 1;
            Object c = queue[child];
            //计算第k个节点的右孩子节点下标,第k个节点的右孩子节点为2k+2
            int right = child + 1;
             //获取左右孩子中最小的节点
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
             //如果x小于c,说明已经是最小堆,直接跳出循环
            if (comparator.compare(x, (E) c) <= 0)
                break;
            //将左右孩子中最小的节点放在第k个节点位置
            queue[k] = c;
            k = child;   //此处k重新赋值
        }
         //将元素x放在第k个节点位置
        queue[k] = x;
    }

    //向下调整构建最小堆
    private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)
             //向下调整构建最小堆
            siftDown(i, (E) queue[i]);
    }

    //返回队列的比较器
    public Comparator<? super E> comparator() {
        return comparator;
    }

    //将队列序列化至输出流中
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out element count, and any hidden stuff
        s.defaultWriteObject();

        // Write out array length, for compatibility with 1.5 version
        s.writeInt(Math.max(2, size + 1));

        // Write out all elements in the "proper order".
        for (int i = 0; i < size; i++)
            s.writeObject(queue[i]);
    }

    //从输入流中反序列化出队列
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in (and discard) array length
        s.readInt();

        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
        queue = new Object[size];

        // Read in all elements.
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();

        // Elements are guaranteed to be in "proper order", but the
        // spec has never explained what that might be.
        heapify();
    }

PriorityQueue插入操作提供了add(E e)、offer(E e)这两个方法,实际上add(E e)内部直接调用了offer(E e),废话不多说,插入方法的源码如下:

    //向队列中插入元素
    public boolean add(E e) {
        return offer(e);
    }

    //向队列中插入元素
    public boolean offer(E e) {
        if (e == null)  //插入的元素不可为null
            throw new NullPointerException();
        modCount++;  //队列结构修改的次数+1
        int i = size;
        if (i >= queue.length)
            grow(i + 1);  //队列需要扩容
        size = i + 1;
        if (i == 0)  //队列为空时,直接将e放在数组第一个位置
            queue[0] = e;
        else
            siftUp(i, e); //将e放在数组的最后一个位置并向上调整构建最小堆
        return true;
    }

PriorityQueue移除操作提供了 poll()、remove(Object o)、 removeEq(Object o)这三个方法,这三个方法的特点如下:

  • poll():如果队列为空,poll()会直接返回null。否则,移除队列头部的元素。
  • remove(Object o):如果队列中存在给定的元素o,就通过for循环确定元素o的下标然后删除。此方法是public类型,可以在类外部直接被使用。
  • removeEq(Object o):此方法是default类型,只可以在本类或同包的类中使用。另外,此方法主要供PriorityQueue的迭代器调用。

这三个方法源码如下:

     //移除队列头部的元素
     public E poll() {
        if (size == 0)
            return null; //如果队列为空直接返回null
        int s = --size;
        modCount++; //队列结构修改的次数+1
        E result = (E) queue[0];
        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
            //向下调整构建最小堆
            siftDown(0, x);
        return result;
    }
  
    //如果队列存在给定的元素o就删除
    public boolean remove(Object o) {
        int i = indexOf(o); //确定元素o的下标
        if (i == -1)  //元素o不存在
            return false;
        else {
            removeAt(i); //从队列中删除元素o
            return true;
        }
    }

    //如果队列存在给定的元素o就删除
    boolean removeEq(Object o) {
        for (int i = 0; i < size; i++) {
            if (o == queue[i]) {  //查找元素o
                removeAt(i);  //从队列中删除元素o
                return true;
            }
        }
        return false;
    }

总结

至此,我们通过分析源码分方式学习了PriorityQueue。再次总结下其特点:

  • PriorityQueue是一个无界队列,没有容量限制。
  • PriorityQueue不允许插入空元素,如果提供了比较器,插入的元素就按照比较器排序。否则,按照自然顺序来排序。
  • PriorityQueue内部没有实现同步,是线程不安全的队列。
  • PriorityQueue的优先级是借助于内部数组的堆排序实现的,队列如果有元素插入或者删除,会进行堆的调整以重新构建最小堆。

关于堆排序,本文不再做介绍,但如果想要理解PriorityQueue内部的优先级实现,需要先了解下堆排序。

由于笔主水平有限,笔误或者不当之处还请批评指正。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值