在開始很重要的集合Map的學習之前,我們先學習一下集合Queue,主要介紹一下集合Queue的幾個重要的實現類。雖然它的內容不多,但它牽涉到了極其重要的資料結構:佇列。所以這次主要針對佇列這種資料結構的使用來介紹Queue中的實現類。
佇列
佇列與棧是相對的一種資料結構。只允許在一端進行插入操作,而在另一端進行刪除操作的線性表。棧的特點是後進先出,而佇列的特點是先進先出。佇列的用處很大,但大多都是在其他的資料結構中,比如,樹的按層遍歷,圖的廣度優先搜尋等都需要使用佇列做為輔助資料結構。
單向佇列
單向佇列比較簡單,只能向隊尾新增元素,從隊頭刪除元素。比如最典型的排隊買票例子,新來的人只能在佇列後面,排到最前邊的人才可以買票,買完了以後,離開隊伍。這個過程是一個非常典型的佇列。
定義佇列的介面:
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的介面定義,這裡節選一部分:
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)。 插入操作的後一種形式是專門為有容量限制的佇列實現而設計的; 在大多數實現中,插入操作不會失敗。
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 以後,當獲取時不好分辨究竟是否正確返回。
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好。
- 最後,送上一個笑話。棧和佇列有什麼區別?吃多了拉就是佇列,吃多了吐就是棧。冬幕節快樂~