Java集合(七) Queue详解

  在开始很重要的集合Map的学习以前,咱们先学习一下集合Queue,主要介绍一下集合Queue的几个重要的实现类。虽然它的内容很少,但它牵涉到了极其重要的数据结构:队列。因此此次主要针对队列这种数据结构的使用来介绍Queue中的实现类。java

队列

  队列与栈是相对的一种数据结构。只容许在一端进行插入操做,而在另外一端进行删除操做的线性表。栈的特色是后进先出,而队列的特色是先进先出。队列的用处很大,但大多都是在其余的数据结构中,好比,树的按层遍历,图的广度优先搜索等都须要使用队列作为辅助数据结构。编程

单向队列

  单向队列比较简单,只能向队尾添加元素,从队头删除元素。好比最典型的排队买票例子,新来的人只能在队列后面,排到最前边的人才能够买票,买完了之后,离开队伍。这个过程是一个很是典型的队列。
  定义队列的接口:数组

public interface Queue {
    public boolean add(Object elem); // 将一个元素放到队尾,若是成功,返回true
    public Object remove(); // 将一个元素从队头删除,若是成功,返回true
}
复制代码

  一个队列只要能入队,和出队就能够了。这个队列的接口就定义好了,具体的实现有不少种办法,例如,可使用数组作存储,可使用链表作存储。
  其实你们页能够看一下JDK源码,在java.util.Queue中,能够看到队列的定义。只是它是泛型的。基本上,Queue.java中定义的接口都是进队,出队。只是行为有所不一样。例如,remove若是失败,会抛出异常,而poll失败则返回null,但它俩其实都是从队头删除元素。安全

双向队列

  若是一个队列的头和尾都支持元素入队,出队,那么这种队列就称为双向队列,英文是Deque。你们能够经过java.util.Deque来查看Deque的接口定义,这里节选一部分:bash

public interface Deque<E> extends Queue<E> {
    /**
     * Inserts the specified element at the front of this deque if it is
     * possible to do so immediately without violating capacity restrictions,
     * throwing an {@code IllegalStateException} if no space is currently
     * available.  When using a capacity-restricted deque, it is generally
     * preferable to use method {@link #offerFirst}.
     *
     * @param e the element to add
     * @throws IllegalStateException if the element cannot be added at this
     *         time due to capacity restrictions
     * @throws ClassCastException if the class of the specified element
     *         prevents it from being added to this deque
     * @throws NullPointerException if the specified element is null and this
     *         deque does not permit null elements
     * @throws IllegalArgumentException if some property of the specified
     *         element prevents it from being added to this deque
     */
    void addFirst(E e);


    void addLast(E e);


    E removeFirst();

    E removeLast();
}
复制代码

  最重要的也就是这4个,一大段英文,没啥意思,其实就是说,addFirst是向队头添加元素,若是不知足条件就会抛异常,而后定义了各类状况下抛出的异常类型。
  只要记住队列是先进先出的数据结构就行了,今天没必要要把这些东西都掌握,一步步来。数据结构

Queue

  Queue也继承自Collection,用来存放等待处理的集合,这种场景通常用于缓冲、并发访问。咱们先看一下官方的定义和类结构:并发

/**
 * A collection designed for holding elements prior to processing.
 * Besides basic {@link java.util.Collection Collection} operations,
 * queues provide additional insertion, extraction, and inspection
 * operations.  Each of these methods exists in two forms: one throws
 * an exception if the operation fails, the other returns a special
 * value (either {@code null} or {@code false}, depending on the
 * operation).  The latter form of the insert operation is designed
 * specifically for use with capacity-restricted {@code Queue}
 * implementations; in most implementations, insert operations cannot
 * fail.
 */
复制代码

  意思大致说Queue是用于在处理以前保存元素的集合。 除了基本的集合操做,队列提供了额外的插入、提取和检查操做。 每一个方法都有两种形式:一种是在操做失败时抛出一个异常,另外一个则返回一个特殊值(根据操做的不一样)(返回null或false)。 插入操做的后一种形式是专门为有容量限制的队列实现而设计的; 在大多数实现中,插入操做不会失败。ide

public interface Queue<E> extends Collection<E> {
    //插入(抛出异常)
    boolean add(E e);
    //插入(返回特殊值)
    boolean offer(E e);
    //移除(抛出异常)
    E remove();
    //移除(返回特殊值)
    E poll();
    //检查(抛出异常)
    E element();
    //检查(返回特殊值)
    E peek();
}
复制代码

  能够看出Queue接口没有什么神秘面纱,都不须要揭开。不存在花里胡哨,就只有这6个方法。额外的添加、删除、查询操做。
  值得一提的是,Queue是个接口,它提供的add,offer方法初衷是但愿子类可以禁止添加元素为null,这样能够避免在查询时返回null到底是正确仍是错误。实际上大多数Queue的实现类的确响应了Queue接口的规定,好比ArrayBlockingQueue,PriorityBlockingQueue等等。
  但仍是有一些实现类没有这样要求,好比LinkedList。
  虽然 LinkedList 没有禁止添加 null,可是通常状况下 Queue 的实现类都不容许添加 null 元素,为啥呢?由于poll(),peek()方法在异常的时候会返回 null,你添加了null 之后,当获取时很差分辨到底是否正确返回。post

PriorityQueue

  PriorityQueue又叫作优先级队列,保存队列元素的顺序不是按照及加入队列的顺序,而是按照队列元素的大小进行从新排序。所以当调用peek()或pool()方法取出队列中头部的元素时,并非取出最早进入队列的元素,而是取出队列的最小元素。
  咱们刚刚才说到队列的特色是先进先出,为何这里就按照大小顺序排序了呢?咱们仍是先看一下它的介绍,直接翻译过来:性能

基于优先级堆的无界的优先级队列。
PriorityQueue的元素根据天然排序进行排序,或者按队列构建时提供的 Comparator进行排序,具体取决于使用的构造方法。
优先队列不容许 null 元素。
经过天然排序的PriorityQueue不容许插入不可比较的对象。
该队列的头是根据指定排序的最小元素。
若是多个元素都是最小值,则头部是其中的一个元素——任意选取一个。
队列检索操做poll、remove、peek和element访问队列头部的元素。
优先队列是无界的,但有一个内部容量,用于管理用于存储队列中元素的数组的大小。
基本上它的大小至少和队列大小同样大。
当元素被添加到优先队列时,它的容量会自动增加。增加策略的细节没有指定。
复制代码

  一句话归纳,PriorityQueue使用了一个高效的数据结构:堆。底层是使用数组保存数据。还会进行排序,优先将元素的最小值存到队头。

PriorityQueue的排序方式

  PriorityQueue中的元素能够默认天然排序或者经过提供的Comparator(比较器)在队列实例化时指定的排序方式进行排序。关于天然排序与Comparator(比较器)能够参考个人上一篇文章Java集合(六) Set详解的讲解。因此这里的用法就不复述了。
  须要注意的是,当PriorityQueue中没有指定的Comparator时,加入PriorityQueue的元素必须实现了Comparable接口(元素是能够进行比较的),不然会致使 ClassCastException。

PriorityQueue本质

  PriorityQueue 本质也是一个动态数组,在这一方面与ArrayList是一致的。看一下它的构造方法:

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,
        // but continues for 1.5 compatibility
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }
复制代码
  • PriorityQueue调用默认的构造方法时,使用默认的初始容量(DEFAULT_IITIAL_CAPACITY = 11)建立一个PriorityQueue,并根据其天然顺序来排序其元素(使用加入其中的集合元素实现的Comparable)。
  • 当使用指定容量的构造方法时,使用指定的初始容量建立一个 PriorityQueue,并根据其天然顺序来排序其元素(使用加入其中的集合元素实现的Comparable)
  • 当使用指定的初始容量建立一个 PriorityQueue,并根据指定的比较器comparator来排序其元素。当添加元素到集合时,会先检查数组是否还有余量,有余量则把新元素加入集合,没余量则调用 grow()方法增长容量,而后调用siftUp将新加入的元素排序插入对应位置。
      除了这些,还要注意的是:
      1.PriorityQueue不是线程安全的。若是多个线程中的任意线程从结构上修改了列表, 则这些线程不该同时访问 PriorityQueue 实例,这时请使用线程安全的PriorityBlockingQueue 类。
      2.不容许插入 null 元素。
      3.PriorityQueue实现插入方法(offer、poll、remove() 和 add 方法) 的时间复杂度是O(log(n)) ;实现 remove(Object) 和 contains(Object) 方法的时间复杂度是O(n) ;实现检索方法(peek、element 和 size)的时间复杂度是O(1)。因此在遍历时,若不须要删除元素,则以peek的方式遍历每一个元素。
      4.方法iterator()中提供的迭代器并不保证以有序的方式遍历PriorityQueue中的元素。

Deque

  Deque接口是Queue接口子接口。它表明一个双端队列。Deque接口在Queue接口的基础上增长了一些针对双端添加和删除元素的方法。LinkedList也实现了Deque接口,因此也能够被看成双端队列使用。也能够看前面的 Java集合(四) LinkedList详解来理解Deque接口。
  先瞄一眼类结构:

public interface Deque<E> extends Queue<E> {
    //从头部插入(抛异常)
    void addFirst(E e);
    //从尾部插入(抛异常)
    void addLast(E e);
    //从头部插入(特殊值)
    boolean offerFirst(E e);
    //从尾部插入(特殊值)
    boolean offerLast(E e);
    //从头部移除(抛异常)
    E removeFirst();
    //从尾部移除(抛异常)
    E removeLast();
    //从头部移除(特殊值)
    E pollFirst();
    //从尾部移除(特殊值)
    E pollLast();
    //从头部查询(抛异常)
    E getFirst();
    //从尾部查询(抛异常)
    E getLast();
    //从头部查询(特殊值)
    E peekFirst();
    //从尾部查询(特殊值)
    E peekLast();
    //(从头至尾遍历列表时,移除列表中第一次出现的指定元素)
    boolean removeFirstOccurrence(Object o);
    //(从头至尾遍历列表时,移除列表中最后一次出现的指定元素)
    boolean removeLastOccurrence(Object o);
    //都没啥难度,不解释了
    boolean add(E e);
    boolean offer(E e);
    E remove();
    E poll();
    E element();
    E peek();
    void push(E e);
    E pop();
    boolean remove(Object o);
    boolean contains(Object o);
    public int size();
    Iterator<E> iterator();
    Iterator<E> descendingIterator();

}
复制代码

  从上面的方法能够看出,Deque不只能够当成双端队列使用,并且能够被当成栈来使用,由于该类中还包含了pop(出栈)、push(入栈)两个方法。其余基本就是方法名后面加上“First”和“Last”代表在哪端操做。

ArrayDeque

  重头戏来了,顾名思义,ArrayDeque使用数组实现的Deque;底层是数组,也是能够指定它的capacity,固然也能够不指定,默认长度是16,根据添加的元素个数,动态扩容。

循环队列

  值得重点介绍的是,ArrayDeque是一个循环队列。它的实现比较高效,它的思路是这样:引入两个游标,head 和 tail,若是向队列里,插入一个元素,就把 tail 向后移动。若是从队列中删除一个元素,就把head向后移动。咱们看一下示意图:

  若是向队列中插入D,那么,队列就会变成这样:
  若是此时,从队列的头部把A删除,那只须要移动head指针便可:
  经过这种方式,就可使元素出队,入队的速度加快了。那若是 tail 已经指向了数组的最后一位怎么办呢?其实呀,只须要将tail从新指向数组的头就能够了。for example,tail已经指向数组最后一位了,再插入一个元素,就会变成这样:
  使用这种方式,就能够循环使用一个数组来实现队列了。
  这里有一个编程上的小技巧,那就是,实现的时候,数组的长度都是2的整数次幂,这样,咱们就可使用(tail++)&(length-1)来计算tail的下一位。好比说:数组长度是1024,2的10次方,若是tail已经指向了数组的最后一位了,那咱们就可使用tail++,而后和1023求“与”,就获得了0,变成了数组的第一项。

扩容

  全部的集合类都会面临一个问题,那就是若是容器中的空间不够了怎么办。这就涉及到扩容的问题。在前面咱们已经说了,咱们要保证数组的长度都是2的整数次幂,那么扩容的时候也很简单,直接把原来的数组长度乘以2就能够了。申请一个长度为原数组两倍的数组,而后把数据拷贝进去就OK了。咱们看一下具体代码:

private void doubleCapacity() {
        assert head == tail;
        int p = head;
        int n = elements.length;
        int r = n - p; // number of elements to the right of p
        int newCapacity = n << 1;
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity];
        System.arraycopy(elements, p, a, 0, r);
        System.arraycopy(elements, 0, a, r, p);
        elements = a;
        head = 0;
        tail = n;
    }
复制代码

  代码没啥难度,先把长度扩一倍,(n<<1),再把数据拷到目标位置。只要把这两个arraycopy方法看懂问题不大。

总个小结

  • 当 Deque 当作 Queue队列使用时(FIFO),添加元素是添加到队尾,删除时删除的是头部元素
  • Deque 也能当Stack栈用(LIFO)。这时入栈、出栈元素都是在 双端队列的头部进行。插一嘴:Stack过于古老,而且实现地很是很差,所以如今基本已经不用了,能够直接用Deque来代替Stack进行栈操做。
  • ArrayDeque不是线程安全的。 看成为栈使用时,性能比Stack好;看成为队列使用时,性能比LinkedList好。
  • 最后,送上一个笑话。栈和队列有什么区别?吃多了拉就是队列,吃多了吐就是栈。冬幕节快乐~
参考

数据结构(三):队列
大话数据结构
Deque 双端队列

相关文章
相关标签/搜索