PriorityQueue 源码分析

2020年2月12日 | 作者 Siran | 2900字 | 阅读大约需要6分钟
归档于 Java | 标签 #Queue

问题

  1. 什么是优先队列?
  2. PriorityQueue线程安全吗?
  3. PriorityQueue是有序的吗?
  4. PriorityQueue如何实现?

简介

PriorityQueue是基于Heap来实现的。 PriorityQueue里的每个元素都会进行排序,每次弹出一个元素要么是最大的要么是最小的,取决于排序规则 PriorityQueue是一个无界的队列,但是内部会有一个容量默认为11。 PriorityQueue确保子节点一定比其父节点大 假设现在往PriorityQueue中插入1,3,4,6,5,1,2


源码分析

主要参数

//默认容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
//底层使用数组来存储数据
transient Object[] queue  // non-private to simplify nested class access
//元素个数
private int size = 0;
//比较器
private final Comparator<? super E> comparator;
//修改次数
transient int modCount = 0; // non-private to simplify nested class access

构造器

//这些构造器可以指定容量和元素的Comparator(不指定就默认元素自身的排序),也可以传入Collection 或者 PriorityQueue
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);
    }
public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        // Note: This restriction of at least one is not actually needed,
        // 容量校验
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }

//传入Collection
public PriorityQueue(Collection<? extends E> c) {
        //<1>.如果这个集合是SortedSet 那么赋值Comparator,调用#initElementsFromCollection()方法初始化
        if (c instanceof SortedSet<?>) {
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
            initElementsFromCollection(ss);
        }
        //<2>.如果就是PriorityQeueue 那么赋值Comparator,调用#initFromPriorityQueue()方法初始化
        else if (c instanceof PriorityQueue<?>) {
            PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            initFromPriorityQueue(pq);
        }
        //<3>.如果是其他集合那么就调用#initFromCollection()方法初始化
        else {
            this.comparator = null;
            initFromCollection(c);
        }
    }

//传入PriorityQueue 同上构造器中的<2>
public PriorityQueue(PriorityQueue<? extends E> c) {
        this.comparator = (Comparator<? super E>) c.comparator();
        initFromPriorityQueue(c);
    }

private void initElementsFromCollection(Collection<? extends E> c) {
        //<1> 转成数组
        Object[] a = c.toArray();
        // If c.toArray incorrectly doesn't return Object[], copy it.
        //<2> 检查
        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();
        //<3> 赋值
        this.queue = a;
        this.size = a.length;
    }

private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
        //<1> 判断类型 如果就是PriorityQueue类型那么说明已经堆化了直接赋值
        if (c.getClass() == PriorityQueue.class) {
            this.queue = c.toArray();
            this.size = c.size();
        } else {
            //<2> 此方法会转成数组并且进行堆化
            initFromCollection(c);
        }
    }

private void initFromCollection(Collection<? extends E> c) {
        //转换为数组
        initElementsFromCollection(c);
        //堆化
        heapify();
    }

//进行堆化(二叉堆) 必须要求子节点比父节点大
private void heapify() {
        //siftDown方法在下面的删除中分析
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }

入队

//添加一个元素,最终是调用#offer()方法
public boolean add(E e) {
        return offer(e);
    }

//这里会判断是否需要扩容,以及会进行自下而上堆化
public boolean offer(E e) {
        //<1> 为null 抛出空指针
        if (e == null)
            throw new NullPointerException();
        modCount++;
        //取元素个数
        int i = size;
        //<2> 元素的个数达到最大容量了则调用#grow()方法进行扩容
        if (i >= queue.length)
            grow(i + 1);
        //元素个数+1
        size = i + 1;
        //<3> 如果第一次添加元素,那么不用进行堆化,直接赋值
        if (i == 0)
            queue[0] = e;
        else
        //<4> 自下而上的进行堆化
            siftUp(i, e);
        return true;
    }

//扩容
private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        //<1> 如果旧容量没有超过64则+2,反之加一半
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));
        // overflow-conscious code
        //<2> 检查是否溢出
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //<3> 创建出一个新容量大小的新数组并把旧数组元素拷贝过去
        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;
    }

//自下而上堆化
private void siftUp(int k, E x) {
        //<1> 如果指定了比较器则调用#siftUpUsingComparator()方法
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
        //<2> 反之调用#siftUpComparable()方法,这里就分析此方法。原理都是一样的
            siftUpComparable(k, x);
    }

private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            //<1> 查找父节点的index
            int parent = (k - 1) >>> 1;
            //<2> 获得父节点的值
            Object e = queue[parent];
            //<3> 添加的值如果 >= 父节点的值,那么满足二叉堆的特性,退出循环
            if (key.compareTo((E) e) >= 0)
                break;
            //<4> 小于父节点的值,那么父节点的值和要插入的值互换位置,
            //依次类推自下而上和父节点进行比较,小就互换位置。
            //所以如果不指定排序规则那么每次取出来的值都是最小的值
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

还是以1,3,4,6,5,1,2 为例子一开始插入1是在4后面的如图所示,但是在进行堆化的时候发现1< 4,互换位置,1的父节点是1 不小于,满足二叉堆的特性。


出队

//弹出头节点,最小值或者最大值并且自上而下的进行堆化
public E poll() {
        //<1> 没有元素 返回null
        if (size == 0)
            return null;
        //<2> 元素个数-1
        int s = --size;
        modCount++;
        //<3> 获得第一个元素
        E result = (E) queue[0];
        //<4> 获取最后一个元素
        E x = (E) queue[s];
        //<5> 把最后一个元素置为null
        queue[s] = null;
        //<6> 自上而下的进行堆化
        if (s != 0)
            siftDown(0, x);
        //<7> 返回第一个元素
        return result;
    }

//和向上堆化一样这里只分析#siftDownComparable()
private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        //<1> 只需要比较一半就行了,因为叶子节点占了一半的元素
        int half = size >>> 1;        // loop while a non-leaf
        while (k < half) {
            //左节点的index
            int child = (k << 1) + 1; // assume left child is least
            //左节点的值
            Object c = queue[child];
            //右节点的index
            int right = child + 1;
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)  
                //<2> 比较左右节点取小值
                c = queue[child = right];
            //<3> 如果最后一个元素的值比当前最小的子节点都小,则退出循环
            if (key.compareTo((E) c) <= 0)
                break;
            //<4> 如果比当前最小的子节点大,则交换位置
            queue[k] = c;
            //<5> 指针移到最小子节点位置,继续往下比较
            k = child;
        }
        //<6> 找到正确的位置,放入元素
        queue[k] = key;
    }

继续以1,3,4,6,5,1,2为例子

现在调用#poll()方法弹出第一个元素,最后一个元素被置为null(这里置为null的意思并不是删除。而是要移动)

因为一个节点下面有两个子节点所以只要比较一半的元素就可以。 以第一个节点开始,获取该节点的左右子节点取小值则为(1),判断如果最后一个元素(2)比当前的最小值(1)还要小直接退出这个循环,并把最后一个元素(2)放到第一个元素的位置上

如果大于当前的最小值(1),则交换位置继续往下比较

最终


删除

//删除给定的元素
public boolean remove(Object o) {
        //<1> 获取元素的下标
        int i = indexOf(o);
        //<2> 不存在返回false
        if (i == -1)
            return false;
        else {
        //<3> 反之调用#removeAt()方法进行删除
            removeAt(i);
            return true;
        }
    }

//通过给定的元素查找元素的下标 时间复杂度为On
private int indexOf(Object o) {
        if (o != null) {
            for (int i = 0; i < size; i++)
                if (o.equals(queue[i]))
                    return i;
        }
        return -1;
    }

private E removeAt(int i) {
        // assert i >= 0 && i < size;
        modCount++;
        int s = --size;
        //<1> 如果是给定的元素是最后一个 直接删除,不用堆化
        if (s == i) // removed last element
            queue[i] = null;
        else {
            //<2> 获得要删除的值
            E moved = (E) queue[s];
            //<3> 最后元素置为null
            queue[s] = null;
            //<4> 向下堆化
            siftDown(i, moved);
            //<5> 此时给定删除元素的位置就是最后一个元素
            if (queue[i] == moved) {
                //<6> 向下堆化
                siftUp(i, moved);
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
    }

获取首元素

//获取头节点 最小值或者是最大值
public E peek() {
        return (size == 0) ? null : (E) queue[0];
    }

总结

  • PriorityQueue不是线程安全的,任何操作都没有使用同步机制。多线程情况下可以使用PriorityBlockingQueue
  • PriorityQueue是无序的,只有第一个元素要么是最小的要么是最大的
  • PriorityQueue入队出对对应堆的插入元素和删除元素,
  • PriorityQueue的入队出队的时间复杂度相比于数组而言慢为O(log(n))