PriorityQueue And Queue


title: PriorityQueue and Queue源码剖析
date: 2018-3-3 23:18:40
categories:
- JDK
tags:
- JDK

- 源码学习

此博文过长,纯属自己记录的笔记,慎入。

ArrayDeque

是一个双向队列,队列的两个口都可以入队和出队操作。再进一步说,其实ArrayDeque可以说成是一个双向循环队列

defination

public class ArrayDeque<E> extends AbstractCollection<E>
                           implements Deque<E>, Cloneable, Serializable
{

}

从ArrayDeque的定义可以看到,它继承AbstractCollection,实现了Deque,Cloneable,Serializable接口。不知道看到这里你会不会发现什么,Deque接口我们在LinkedList中见过,LinkedList也是实现Deque接口的。Deque是一个双端队列,它实现于Queue接口,就是在队列的同一端即可以入队又可以出队,所以Deque即可以作为队列又可以作为栈使用。
欲知Dequeue先知Queue,jdk1.8源码如下:

public interface Queue<E> extends Collection<E> {
  // 增加一个元素到队尾,如果队列已满,则抛出一个IIIegaISlabEepeplian异常
    boolean add(E e);
    // 添加一个元素到队尾并返回true,如果队列已满,则返回false
    boolean offer(E e);
    // 移除并返回队列头部的元素,如果队列为空,则抛出一个NoSuchElementException异常
    E remove();
    // 移除并返问队列头部的元素,如果队列为空,则返回null
    E poll();
    // 返回队列头部的元素,如果队列为空,则抛出一个NoSuchElementException异常
    E element();
    // 返问队列头部的元素,如果队列为空,则返回null
    E peek()
}

Queue的定义和Stack的定义差不多了。ArrayDeque并不是一个固定大小的队列,每次队列满了就会进行扩容,除非扩容至超过int的边界,才会抛出异常。所以这里的add和offer几乎是没有区别的。

// 底层用数组存储元素
private transient E[] elements;
// 队列的头部元素索引(即将pop出的一个)
private transient int head;
// 队列下一个要添加的元素索引
private transient int tail;
// 最小的初始化容量大小,需要为2的n次幂
private static final int MIN_INITIAL_CAPACITY = 8;

这里的MIN_INITIAL_CAPACITY要求是2的整数次幂,我们知道在Hashmap里面,bucket桶的长度是要求2的整数次幂,这里又是什么原因呢?
看一下ArrayDeque的构造函数就知道了。

/**
 * 默认构造方法,数组的初始容量为16
 */
public ArrayDeque() {
    elements = (E[]) new Object[16];
}

/**
 * 使用一个指定的初始容量构造一个ArrayDeque
 */
public ArrayDeque( int numElements) {
    allocateElements(numElements);
}

/**
 * 构造一个指定Collection集合参数的ArrayDeque
 */
public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}

/**
 * 分配合适容量大小的数组,确保初始容量是大于指定numElements的最小的2的n次幂
 */
private void allocateElements(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // 找到大于指定容量的最小的2的n次幂
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    // 如果指定的容量小于初始容量8,则执行一下if中的逻辑操作
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1; // Good luck allocating 2 ^ 30 elements
    }
    elements = (E[]) new Object[initialCapacity];
}

这里的代码是抄别人的,期初我也看不懂,大概的意思就是说,每次需要找到刚好大于numElementsd的2的整数次幂的整数,这些位运算,你自己去算吧。

入队操作

我们经常用到就是 add(), offer(),分别调用 addLast(), offerLast()这几个方法了。不同的是 add()系列的方法会抛异常,而offer()系列的会返回true or false. remove() poll()同理。

/**
     * 增加一个元素,如果队列已满,则抛出一个IIIegaISlabEepeplian异常
     */
    public boolean add(E e) {
        // 调用addLast方法,将元素添加到队尾
        addLast(e);
        return true;
    }

     /**
     * 添加一个元素
     */
    public boolean offer(E e) {
        // 调用offerLast方法,将元素添加到队尾
        return offerLast(e);
    }

    /**
     * 在队尾添加一个元素
     */
    public boolean offerLast(E e) {
        // 调用addLast方法,将元素添加到队尾
        addLast(e);
        return true;
    }

    /**
     * 将元素添加到队尾
     */
    public void addLast(E e) {
        // 如果元素为null,咋抛出空指针异常
        if (e == null)
            throw new NullPointerException();
        // 将元素e放到数组的tail位置
        elements[tail ] = e;
        // 判断tail和head是否相等,如果相等则对数组进行扩容
        if ( (tail = (tail + 1) & ( elements.length - 1)) == head)
            // 进行两倍扩容
            doubleCapacity();
    }

(tail = (tail + 1) & ( elements.length - 1)) == head)最为精妙!

偷张别人的图解释一下。
我们假设队列初始容量为8,初始化添加,A,B,C,D四元素。分别对应数组下标0,1,2,3,但是因为这是双端队列,经过一段时间的出队,入队,可能会变成上图,右边的情况。
那么tail到达了数组末尾,再添加元素的话,怎么办, 直接扩容吗? 前面还有一段呢? 答案是不会扩容,会把元素填充到前面缺失的位置,一直到这个数组是真正的满了,才会扩容。(tail = (tail + 1) & ( elements.length - 1))只会在 head 和 tail指针相邻才会 == head然后就开始扩容。Javaer们把这个叫做,循环数组!

ArrayDeque的扩容

/**
 * 数组将要满了的时候(tail==head)将,数组进行2倍扩容
 */
private void doubleCapacity() {
    // 验证head和tail是否相等
    assert head == tail;
    int p = head ;
    // 记录数组的长度
    int n = elements .length;
    // 计算head后面的元素个数,这里没有采用jdk中自带的英文注释right,是因为所谓队列的上下左右,只是我们看的方位不同而已,如果上面画的图,这里就应该是left而非right
    int r = n - p; // number of elements to the right of p
    // 将数组长度扩大2倍
    int newCapacity = n << 1;
    // 如果此时长度小于0,则抛出IllegalStateException异常,什么时候newCapacity会小于0呢,前面我们说过了int值<<1越界
    if (newCapacity < 0)
        throw new IllegalStateException( "Sorry, deque too big" );
    // 创建一个长度是原数组大小2倍的新数组
    Object[] a = new Object[newCapacity];
    // 将原数组head后的元素都拷贝值新数组
    System. arraycopy(elements, p, a, 0, r);
    // 将原数组head前的元素都拷贝到新数组
    System. arraycopy(elements, 0, a, r, p);
    // 将新数组赋值给elements
    elements = (E[])a;
    // 重置head为数组的第一个位置索引0
    head = 0;
    // 重置tail为数组的最后一个位置索引+1((length - 1) + 1)
    tail = n;
}

两次复制,是因为,这个数组是分为两段的。

出队

/**
 * 移除并返回队列头部的元素,如果队列为空,则抛出一个NoSuchElementException异常
 */
public E remove() {
    // 调用removeFirst方法,移除队头的元素
    return removeFirst();
}

/**
 * @throws NoSuchElementException {@inheritDoc}
 */
public E removeFirst() {
    // 调用pollFirst方法,移除并返回队头的元素
    E x = pollFirst();
    // 如果队列为空,则抛出NoSuchElementException异常
    if (x == null)
        throw new NoSuchElementException();
    return x;
}

/**
 * 移除并返问队列头部的元素,如果队列为空,则返回null
 */
public E poll() {
    // 调用pollFirst方法,移除并返回队头的元素
    return pollFirst();
}

public E pollFirst() {
    int h = head ;
    // 取出数组队头位置的元素
    E result = elements[h]; // Element is null if deque empty
    // 如果数组队头位置没有元素,则返回null值
    if (result == null)
        return null;
    // 将数组队头位置置空,也就是删除元素
    elements[h] = null;     // Must null out slot
    // 将head指针往前移动一个位置
    head = (h + 1) & (elements .length - 1);
    // 将队头元素返回
    return result;
}

PriorityQueue

自己在写一些算法题目的时候,特别是一些面试刁钻的搜索问题时(google有一道无序数组,寻找第K大元素,就是用构造K堆来实现的),都会用到堆排序,其实JDK已经为我们封装好了一个类似对结构的优先队列,就是这个PriorityQueue 也叫 二叉堆。

其实就是一颗完全二叉树(二叉堆),特点:在第n层深度被填满之前,不会开始填第n+1层深度,而且元素插入是从左往右填满。
  基于这个特点,二叉堆又可以用数组来表示而不是用链表

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

底层存储

// 默认初始化大小 
privatestaticfinalintDEFAULT_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.
 */
private transient Object[] queue ;

// 队列的大小
private int size = 0;

// 比较器
private final Comparator<? super E> comparator;

// 版本号
private transient int modCount = 0;

看上面的英文注释,我的英语不好也看懂了,哈哈。

PriorityQueue()

public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null); // null 为比较器,默认null
}

public PriorityQueue( int initialCapacity) {
    this(initialCapacity, null);
}

/**
 * 使用指定的初始大小和比较器来构造一个优先队列
 */
public PriorityQueue( int initialCapacity,
                     Comparator<? super E> comparator) {
    // Note: This restriction of at least one is not actually needed,
    // but continues for 1.5 compatibility
    // 初始大小不允许小于1
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    // 使用指定初始大小创建数组
    this.queue = new Object[initialCapacity];
    // 初始化比较器
    this.comparator = comparator;
}

/**
 * 构造一个指定Collection集合参数的优先队列
 */
public PriorityQueue(Collection<? extends E> c) {
    // 从集合c中初始化数据到队列
    initFromCollection(c);
    // 如果集合c是包含比较器Comparator的(SortedSet/PriorityQueue),则使用集合c的比较器来初始化队列的Comparator
    if (c instanceof SortedSet)
        comparator = (Comparator<? super E>)
            ((SortedSet<? extends E>)c).comparator();
    else if (c instanceof PriorityQueue)
        comparator = (Comparator<? super E>)
            ((PriorityQueue<? extends E>)c).comparator();
    //  如果集合c没有包含比较器,则默认比较器Comparator为空
    else {
        comparator = null;
        // 调用heapify方法重新将数据调整为一个二叉堆
        heapify();
    }
}

/**
 * 构造一个指定PriorityQueue参数的优先队列
 */
public PriorityQueue(PriorityQueue<? extends E> c) {
    comparator = (Comparator<? super E>)c.comparator();
    initFromCollection(c);
}

/**
 * 构造一个指定SortedSet参数的优先队列
 */
public PriorityQueue(SortedSet<? extends E> c) {
    comparator = (Comparator<? super E>)c.comparator();
    initFromCollection(c);
}

/**
 * 从集合中初始化数据到队列
 */
private void initFromCollection(Collection<? extends E> c) {
    // 将集合Collection转换为数组a
    Object[] a = c.toArray();
    // If c.toArray incorrectly doesn't return Object[], copy it.
    // 如果转换后的数组a类型不是Object数组,则转换为Object数组
    if (a.getClass() != Object[].class)
        a = Arrays. copyOf(a, a.length, Object[]. class);
    // 将数组a赋值给队列的底层数组queue
    queue = a;
    // 将队列的元素个数设置为数组a的长度
    size = a.length ;
}

add() 和 offer()

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
  //如果压入的元素为null 抛出异常      
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
        //如果数组的大小不够扩充
    size = i + 1;
    if (i == 0)
        queue[0] = e;
        //如果只有一个元素之间放在堆顶
    else
        siftUp(i, e);
        //否则调用siftUp函数从下往上调整堆。
    return true;
}

这两个方法都是向这个小堆里面添加一个元素,add()方法体内,也是调用的offer()啦。

public boolean add(E e) {
    return offer(e);
}

offer()的源码是这样的:

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
  //如果压入的元素为null 抛出异常      
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
        //如果数组的大小不够扩充
    size = i + 1;
    if (i == 0)
        queue[0] = e;
        //如果只有一个元素之间放在堆顶
    else
        siftUp(i, e);
        //否则调用siftUp函数从下往上调整堆。
    return true;
}

和我自己写得堆排序里面的,交换过程差不多。
1. 优先队列无法存放 null
2. 元素进堆之后,如果数组大小不够,进行扩充数组,
3. 从下往上,调整堆。siftUp(i,e)

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

就是一个不断比较交换的过程,不再赘述。

Poll() remove()

poll 方法每次从 PriorityQueue 的头部删除一个节点,也就是从小顶堆的堆顶删除一个节点,而remove()不仅可以删除头节点而且还可以用 remove(Object o) 来删除堆中的与给定对象相同的最先出现的对象。
poll() 方法就很好理解了,删除堆顶元素,然后再调整堆。

public E poll() {
    if (size == 0)
        return null;
    //如果堆大小为0则返回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;
}

同理,siftDown()是从堆顶往下调整堆。

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

大小变换

如果想利用 priorityQueue来构造最大堆的话,自己写一个Comparator传入构造函数就行了,具体的构造函数,前面已经说过了啊。

public PriorityQueue(int initialCapacity,
                     Comparator<? super E> comparator) {
    // Note: This restriction of at least one is not actually needed,
    // but continues for 1.5 compatibility
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

猜你喜欢

转载自blog.csdn.net/qq_33797928/article/details/79896838
今日推荐