Java 集合深入理解 :优先队列(PriorityQueue)之源码解读,及最小顶堆实现研究

Posted 踩踩踩从踩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合深入理解 :优先队列(PriorityQueue)之源码解读,及最小顶堆实现研究相关的知识,希望对你有一定的参考价值。

前言

queue 接口
队列就是一个先入先出(FIFO)的数据结构
AbstractQueue类
也是继承自AbstractCollection 并实现queue接口的,在集合基础增加一些队列自带的方法 的基础实现
PriorityQueue类
继承AbstractQueue ,用object数组存储数组,通过二叉小顶堆实现,用一棵完全二叉树表示。(对于平常我们研究的树,大部分都是链表实现的,对于堆实现还是很有研究的意义)
这种实现:
它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()或poll()方法,返回的总是优先级最高的元素。而对于什么是优先级最高的元素,在数组中看起来不是最顺序的。

先写一个例子

public static void main(String[] args) {
		PriorityQueue<Integer> q = new PriorityQueue<>();
        q.offer(3);
        q.offer(1);
        q.offer(2);
        System.out.println(q.poll()); // 1
        System.out.println(q.poll()); // 2
        System.out.println(q.poll()); // 3
        System.out.println(q.poll()); // null,因为队列为空
    }

看例子 默认会将integer在offer的时候就进行排序,自动插入到合适的位置保证队列有序。

PriorityQueue 的注释

	/**
	*基于优先级堆的无限优先级{@linkplain Queue}。
	*优先级队列的元素根据它们的优先级排序
	*{@linkplain Comparable natural ordering},或通过{@link Comparator}
	*在队列构造时提供,具体取决于所使用的构造函数用过的。优先级队列不允许{@code null}元素。
	*依赖于自然排序的优先级队列也不允许插入不可比较的对象(这样做可能会导致
	*{@code ClassCastException})。
	*<p>这个队列的<em>头是<em>最少的<em>元素
	*关于指定的顺序。如果有多个元素为了最小的价值,头部是其中的一个元素——领带是
	*任意破坏。队列检索操作{@code poll},{@code remove}、{@code peek}和{@code element}访问
	*位于队列头部的元素。
	*<p>优先级队列是无界的,但具有内部
	*<i>容量</i>控制用于存储队列中的元素。它总是至少和队列一样大
	*大小。当元素添加到优先级队列时,它的容量自动增长。增长政策的细节并不清楚指定。
	*<p>这个类及其迭代器实现了
	*<em>可选的{@link集合}和{@link集合的方法迭代器}接口。方法{@link中提供的迭代器
	*#iterator()}不能保证遍历按任何特定顺序排列的优先级队列。如果您需要订购
	*遍历时,请考虑使用{@code Arrays.sort(pq.toArray())}。
	*<p><strong>请注意,此实现不同步。</strong>多个线程不应访问{@code PriorityQueue}
	*如果任何线程修改队列,则同时执行。相反,使用线程安全的{@link
	*java.util.concurrent.PriorityBlockingQueue}类。
	*<p>实施说明:此实施提供
	*O(log(n))排队和出列方法的时间
	*({@code offer}、{@code poll}、{@code remove()}和{@code add});
	*{@code remove(Object)}和{@code contains(Object)}的线性时间
	*方法;检索方法的时间常数({@code peek}、{@code element}和{@code size})。
	* <p>This class is a member of the
	* <a href="{@docRoot}/../technotes/guides/collections/index.html">
	* Java Collections Framework</a>.
	*
	* @since 1.5
	* @author Josh Bloch, Doug Lea
	* @param <E> the type of elements held in this collection
	*/

总结注释上面的意思 Java1.5中引入的
1.基于优先级堆进行排序
2.优先级队列不允许{@code null}元素 依赖于自然排序的优先级队列也不允许插入不可比较的对象 就可能导致ClassCastException异常
3.优先级队列是无界的,但具有内部 容量控制用于存储队列中的元素。它总是至少和队列一样大
4.此实现不同步如果想使用线程安全的,则使用PriorityBlockingQueue类

实现原理

可以用一棵完全二叉树表示(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。

在这里插入图片描述
根据例图来看
PriorityQueue的底层实现为平衡二进制堆的优先级队列:
队列[n]的子级是队列[2n+1]和队列[2(n+1)]。
leftNo = [2n+1]
rightNo = [2
(n+1)]
队列n也就是父节点
通过上述2个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。
PriorityQueue_的peek()和element操作是常数时间,add(), offer(), 无参数的remove()以及poll()方法的时间复杂度都是_log(N) 。
不理解_log(N) 参考
时间复杂度 O(log n) 意味着什么

属性分析

  private static final int DEFAULT_INITIAL_CAPACITY = 11;
 /**
    *表示为平衡二进制堆的优先级队列:队列[n]的子级是队列[2*n+1]和队列[2*(n+1)]。这个
    *优先级队列由比较器或元素的自然排序,如果比较器为空:对于堆和n的每个后代d,n<=d。具有
    *最小值在队列[0]中,假设该队列为非空。
     */
    transient Object[] queue; // 非私有以简化嵌套类访问

    /**
     * 优先级队列中的元素数。
     */
    private int size = 0;

    /**
     * 比较器,如果优先级队列使用元素
     * natural ordering.
     */
    private final Comparator<? super E> comparator;
  /**
     * 此优先级队列被删除的次数
     * <i> 结构上修改的</i>。请参见抽象列表。
     */
    transient int modCount = 0; // 非私有以简化嵌套类访问

默认容量

默认容量为11,该容量是初始化容器时进行设置的

transient修饰的变量不参与序列化和反序列化
在序列化时,定义了两个方法writeObject和readObject,实现了自己可控制的序列化,而达到定制的效果,也就是不跟据queue的大小进行序列化,而是根据 size进行的

   /**
     * 将此队列保存到流中(即序列化它)。
     *
     * @serialData The length of the array backing the instance is
     *             emitted (int), followed by all of its elements
     *             (each an {@code Object}) in the proper order.
     * @param s the stream
     */
 private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // 写下元素计数和任何隐藏的东西
        s.defaultWriteObject();

        // 写出数组长度,以便与1.5版本兼容
        s.writeInt(Math.max(2, size + 1));

        // 按“适当的顺序”写出所有的内容。
        for (int i = 0; i < size; i++)
            s.writeObject(queue[i]);
    }

 /**
     * 从流中重建{@code PriorityQueue}实例
     * (that is, deserializes it).
     *
     * @param s the stream
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // 阅读的大小,以及任何隐藏的东西
        s.defaultReadObject();

        // 读入(和丢弃)数组长度
        s.readInt();

        queue = new Object[size];

        // 读入所有元素。
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();

        // 元素保证“有序”,但
        // spec从未解释过这可能是什么
        heapify();
    }

理解作者所写的平衡二进制堆的优先级队列
先从什么是堆说起
堆是一种非线性结构,(分析堆的数组实现)可以把堆看作一个数组,也可以被看作一个完全二叉树,通俗来讲堆其实就是利用完全二叉树的结构来维护的一维数组
按照堆的特点可以把堆分为大顶堆和小顶堆
大顶堆:每个结点的值都大于或等于其左右孩子结点的值
小顶堆:每个结点的值都小于或等于其左右孩子结点的值
堆的这种特性非常的有用,堆常常被当做优先队列使用,因为可以快速的访问到“最重要”的元素

优先级队列
是由小顶堆实现的数据结构。

构造方法

  1. 不带参数的构造方法
    设置默认的容量1 以及默认的Comparator
  2. 带默认容量的构造方法,
    设置默认的Comparator
  3. 指定Comparator和容量
  4. PriorityQueue 构造方法中放集合
    利用initElementsFromCollection 初始化方法初始化数据,并赋值pq.comparator(),没有构造器则设置为null
public PriorityQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }
 public PriorityQueue(int initialCapacity) {
        this(initialCapacity, null);
    }
  public PriorityQueue(Comparator<? super E> comparator) {
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }
/**
     * 创建具有指定初始容量的{@code PriorityQueue}
     * 它根据指定的比较器对其元素进行排序。
     *
     * @param  initialCapacity此优先级队列的初始容量
     * @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.
     * @throws IllegalArgumentException if {@code initialCapacity} is
     *         less than 1
     */
    public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        // 注意:实际上不需要至少一个的限制,
        // 但1.5的兼容性仍在继续
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }
 /**
     * 创建包含中元素的{@code PriorityQueue}
     * 指定的集合。如果指定的集合是
     * 一个{@link SortedSet}或者是另一个{@code PriorityQueue},这个
     * 优先级队列将按照相同的顺序排序。否则,此优先级队列将根据
     * 元素的{@linkplain可比自然排序}。
     *
     * @param  c the collection whose elements are to be placed
     *         into this priority queue
     * @throws ClassCastException if elements of the specified collection
     *         cannot be compared to one another according to the priority
     *         queue's ordering
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    @SuppressWarnings("unchecked")
    public PriorityQueue(Collection<? extends E> c) {
        if (c instanceof SortedSet<?>) {
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
            initElementsFromCollection(ss);
        }
        else if (c instanceof PriorityQueue<?>) {
            PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            initFromPriorityQueue(pq);
        }
        else {
            this.comparator = null;
            initFromCollection(c);
        }
    }
  private void initElementsFromCollection(Collection<? extends E> c) {
        Object[] a = c.toArray();
        // 如果c.toArray不正确地返回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++)
                if (a[i] == null)
                    throw new NullPointerException();
        this.queue = a;
        this.size = a.length;
    }
  1. initElementsFromCollection方法
    不调整数据顺序,直接将 初始化 集合中的数据赋值给属性queue 和size

//initFromCollection 调整数据结构

  /**
     * 在整个树中建立堆不变量(如上所述),
     * 在调用之前不考虑元素的顺序。
     */
    @SuppressWarnings("unchecked")
    private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }
  /**
     * 在位置k处插入项目x,保持堆不变
     * 重复将x降级到树下,直到它小于或等于
     * 等于它的孩子或是一片叶子。
     *
     * @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);
    }
 

siftDown方法
这就是保持堆不变 做调整树的方法,在remove()和poll() 方法中也会调用的,这里先不 深究

add 和offer 方法

  /**
     * 将指定的元素插入此优先级队列。
     *
     * @return {@code true} (as specified by {@link Collection#add})
     * @throws ClassCastException 如果指定的元素不能
     *         与当前在此优先级队列中的元素进行比较
     *         根据优先级队列的顺序
     * @throws NullPointerException if the specified element is null
     */
    public boolean add(E e) {
        return offer(e);
    }
     /**
     * 将指定的元素插入此优先级队列。
     *
     * @return {@code true} (as specified by {@link Queue#offer})
     * @throws ClassCastException 如果指定的元素不能
     *         与当前在此优先级队列中的元素进行比较
     *         根据优先级队列的顺序
     * @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;
    }
  /**
     * 增加阵列的容量。
     *
     * @param 最小容量所需的最小容量
     */
    private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // 两倍大小,如果小;其他增长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);
    }
    
//arrylist中代码
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
 private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 有溢出意识的代码
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

从上面的代码分析

  • offer 方法 如果添加e值为null则直接抛出 NullPointerException 异常
  • i >= queue.length 才判断需要扩展队列长度,当前size只要等于队列的大小就进行扩容,
    这个可以arrayList中的动态扩容比对看一下
  • arraylist中会先从size加+1判断 在和默认容量比较取最大容量,在与数组大小对比扩容,并扩展老容量的50%,其实都是达到动态扩容的效果
  • PriorityQueue扩容则直接判断老容量是否小于64,然后在采用不同的扩容方式,小于64直接扩容两倍,大于64,直接扩容50%
  • 只有当所需最小的容量大于最大容量,才使用最小容量去计算新容量
    PriorityQueue的动态扩容
    在这里插入图片描述

siftUp方法

  /**
     * 在位置k处插入项x,保持堆不变
     * 在树上提升x,直到它大于或等于
     * 它的父级,或者是根。
     *
     * 简化和加速强制和比较。这个
     * 可比版本和比较版本分为不同的版本
     * 其他方面相同的方法(类似于siftDown。)
     *
     * @param k要填补的职位
     * @param x the item to insert
     */
    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

这是一个最小根堆,从某个节点开始往上对比,一旦父节点比它大,则交换,直到比它小为止。 这可能比较难理解,就用一个图画来展示。

从末尾添加一个节点
在这里插入图片描述
往上和父节点比较交换数据

在这里插入图片描述
在这里插入图片描述
完成交换实现代码
对于没有传入Comparator 的
1.默认需要传入对象需要支持Comparable,不然会出现转换异常,并且,对比也是调用的是对象的compareTo方法
2. 在这里 k是size, 这个公式是求左右节点的2*(n+1) 的变形
从k指定的位置开始,将x逐层与当前点的parent进行比较并交换,直到满足x >= queue[parent]为止

  @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;//parentNo = (nodeNo-1)/2
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

这就是整个 add offer操作,重要的一点主要是 利用最小堆调整数据位置的

peek 和indexOf和element()方法

** peek 方法 获取到 第一个数据数据,先进先出 **

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

AbstractQueue 类中element方法
当没有数组则抛NoSuchElementException异常

   public E element() {
        E x = peek();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

poll()和remove()方法

remove()和poll()方法的语义也完全相同,都是获取并删除队首元素,区别是前者当方法失败时前者抛出异常,后者返回null。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。

AbstractQueue 中实现remove 方法

  public E remove() {
        E x = poll();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

数据为空则抛出NoSuchElementException异常

poll方法源码

  @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;
    }

modCount 变量加+1,size -1 将头数据给获取

以上是关于Java 集合深入理解 :优先队列(PriorityQueue)之源码解读,及最小顶堆实现研究的主要内容,如果未能解决你的问题,请参考以下文章

Java 集合深入理解 :LinkedList链表源码研究,及双向队列如何实现

Java 集合深入理解 (十三) :ArrayDeque实现原理研究,及动态扩容双端队列和单队列和栈比较

Java 集合深入理解 :AbstractList类

使用优先队列对链表进行排序

优先队列不打印推入其中的值[关闭]

PriorityQueue(优先级队列总结)