java中PriorityQueue源码阅读

PriorityQueue实际上是一个小根堆(大根堆),具体堆顶元素是最大元素还是最小元素取决于你传入的比较器,默认是小根堆,即每次堆顶拿到的是最小的元素。

我们来看一下实例

    public static void main(String[] args) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[0]-b[0]);
        q.add(new int[]{2,5});
        q.add(new int[]{1,6});
        q.add(new int[]{3,2});
        q.add(new int[]{4,9});
        q.add(new int[]{5,1});
        System.out.println("hello");
    }

运行上述代码,可以得到一个小根堆,并且是根据int数组的第一个元素进行比较的。

 如上图所示,左边是在内存中的实际存储顺序,右边则是逻辑上的存储顺序。

那么如果我们改变一下改变的元素,使用数组中第二个元素进行比较会发生什么呢

    public static void main(String[] args) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
        q.add(new int[]{2,5});
        q.add(new int[]{1,6});
        q.add(new int[]{3,2});
        q.add(new int[]{4,9});
        q.add(new int[]{5,1});
        System.out.println("hello");
    }

运行上述代码,可以得到如下结果

可以发现,在实际存储时,第二个元素并不是严格有序的,在逻辑上则表示为某个结点的值比其孩子结点要小,这样就能保证每次取最顶上的元素都是最小的。

接下来我们来探讨一下比较常用方法的源码。

首先是属性

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

    private static final long serialVersionUID = -7720805057305804111L;

    private static final int DEFAULT_INITIAL_CAPACITY = 11;

    /**
     * Priority queue represented as a balanced binary heap: the two
     * children of queue[n] are queue[2*n+1] and queue[2*(n+1)].  The
     * priority queue is ordered by comparator, or by the elements'
     * natural ordering, if comparator is null: For each node n in the
     * heap and each descendant d of n, n <= d.  The element with the
     * lowest value is in queue[0], assuming the queue is nonempty.
     */
    transient Object[] queue; // non-private to simplify nested class access

    /**
     * The number of elements in the priority queue.
     */
    private int size = 0;

    /**
     * The comparator, or null if priority queue uses elements'
     * natural ordering.
     */
    private final Comparator<? super E> comparator;

    /**
     * The number of times this priority queue has been
     * <i>structurally modified</i>.  See AbstractList for gory details.
     */
    transient int modCount = 0; // non-private to simplify nested class access
    ...
}

可以看到,PriorityQueue的内部存储结构是Object数组,并且在没有指定数组容量的时候,默认容量是11。

构造函数

PriorityQueue的构造函数有很多,我们就看文章开头例子用到的那个,那个也是最常用的。

    /**
     * Creates a {@code PriorityQueue} with the default initial capacity and
     * whose elements are ordered according to the specified comparator.
     *
     * @param  comparator the comparator that will be used to order this
     *         priority queue.  If {@code null}, the {@linkplain Comparable
     *         natural ordering} of the elements will be used.
     * @since 1.8
     */
    public PriorityQueue(Comparator<? super E> comparator) {
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }

可以发现,我们需要传入一个比较器作为参数。还记得开头我们的初始化方法吗,我们使用了lambda表达式,并且这种写法等价于下的写法

    public static void main(String[] args) {
        //PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
        PriorityQueue<int[]> q = new PriorityQueue<>(new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[0]-o2[0];
            }
        });
        q.add(new int[]{2,5});
        q.add(new int[]{1,6});
        q.add(new int[]{3,2});
        q.add(new int[]{4,9});
        q.add(new int[]{5,1});
        System.out.println("hello");
    }

也就是说,继承Comparator<int[]>接口,并且实现其中的compare()方法。

扩容方法

既然是使用数组作为基本的存储结构,那么就需要有扩容的方法

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * Increases the capacity of the array.
     *
     * @param minCapacity the desired minimum capacity
     */
    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;
    }

我们就看最核心的扩容方案

int newCapacity = oldCapacity+((oldCapacity<64)?(oldCapacity+2):(oldCapacity>>1));

如果原来的容量小于64,那么新容量就是2*oldCapacity+2

如果原来的容量大于等于64,那么新容量就是1.5*oldCapacity

常用方法

    /**
     * Inserts the specified element into this priority queue.
     *
     * @return {@code true} (as specified by {@link Collection#add})
     * @throws ClassCastException if the specified element cannot be
     *         compared with elements currently in this priority queue
     *         according to the priority queue's ordering
     * @throws NullPointerException if the specified element is null
     */
    public boolean add(E e) {
        return offer(e);
    }

    /**
     * Inserts the specified element into this priority queue.
     *
     * @return {@code true} (as specified by {@link Queue#offer})
     * @throws ClassCastException if the specified element cannot be
     *         compared with elements currently in this priority queue
     *         according to the priority queue's ordering
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
    }

add(E e)offer(E e)方法用于添加元素,其基本逻辑是首先判断容量是否足够,如果不足够就添加容量,然后将元素插入到最后的位置,再调整整个堆。

    /**
     * Inserts item x at position k, maintaining heap invariant by
     * promoting x up the tree until it is greater than or equal to
     * its parent, or is the root.
     *
     * To simplify and speed up coercions and comparisons. the
     * Comparable and Comparator versions are separated into different
     * methods that are otherwise identical. (Similarly for siftDown.)
     *
     * @param k the position to fill
     * @param x the item to insert
     */
    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

    @SuppressWarnings("unchecked")
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

    @SuppressWarnings("unchecked")
    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            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;
    }

siftUp(int k, E x)方法中有一个判断,如果我们指定了比较器,就用我们所指定的比较器,否则就是用元素的自然顺序(按照元素的本身的比较规则去比较)。

其中使用比较器的方法是siftUpUsingComparator(int k, E x),我们重点看这个方法。

这个方法的逻辑是,对于位置为k的结点,比较其和父结点的大小,如果比其父结点小,那么就将父结点交换到其原来位置。不断重复这个过程,直到其父结点比其要小或者已经到达了根结点为止。此时,k所指向的位置就是元素x应该待的位置。

    @SuppressWarnings("unchecked")
    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;
    }

poll()方法用于弹出根结点,并且将最后一个元素放到根结点的位置开始向下调整。poll()调用了siftDown(int k, E x)方法来向下调整,接下来我们来看一下siftDown(int k, E x)方法

    /**
     * Inserts item x at position k, maintaining heap invariant by
     * demoting x down the tree repeatedly until it is less than or
     * equal to its children or is a leaf.
     *
     * @param k the position to fill
     * @param x the item to insert
     */
    private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

    @SuppressWarnings("unchecked")
    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) {
            int child = (k << 1) + 1; // assume left child is least
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            if (key.compareTo((E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

    @SuppressWarnings("unchecked")
    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = x;
    }

siftDown(int k, E x)方法的逻辑与siftUp(int k, E x)方法是非常类似的。

我们重点看一下siftDownUsingComparator(int k, E x)这个方法

首先half指向的是第一个叶子结点,我们需要调整所有非叶子结点。

对于每一个待调整的结点,首先找出他的左右孩子中较小的那个,如果这个较小的孩子结点比x小,那么就将较小的孩子结点移动到k处,令k指向较小孩子结点。

重复上述循环,直到x比位置k的孩子结点都要小或者k大于等于half了,那么就结束循环,此时k所指向的位置就是该元素应该待的位置。

上述逻辑实际上就是堆排序的过程,如果觉得我说的不清楚可以看看下面教程

图解排序算法(三)之堆排序 - dreamcatcher-cx - 博客园

总结

PriorityQueue在算法题中还是挺常用的,今天就遇到了一道使用先序队列的贪心算法题

https://leetcode-cn.com/problems/maximum-number-of-eaten-apples/

希望这篇文章能对你有帮助O(∩_∩)O哈哈~

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值