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 雙端隊列

相關文章
相關標籤/搜索