今天我們來介紹下集合Queue中的幾個重要的實現類。關于集合Queue中的內容就比較少了。主要是針對隊列這種數據結構的使用來介紹Queue中的實現類。
Queue用于模擬隊列這種數據結構,隊列通常是指“先進先出”(FIFO)的容器。新元素插入(offer)到隊列的尾部,訪問元素(poll)操作會返回隊列頭部的元素。通常,隊列不允許隨機訪問隊列中的元素。
這種結構就如同我們生活中的排隊一樣。
下面我們就來介紹Queue中的一個重要的實現類PriorityQueue。
PriorityQueue
PriorityQueue保存隊列元素的順序不是按加入隊列的順序,而是按隊列元素的大小進行重新排序。因此當調用peek()或pool()方法取出隊列中頭部的元素時,并不是取出最先進入隊列的元素,而是取出隊列中的最小的元素。
PriorityQueue的排序方式
PriorityQueue中的元素可以默認自然排序(也就是數字默認是小的在隊列頭,字符串則按字典序排列)或者通過提供的Comparator(比較器)在隊列實例化時指定的排序方式。關于自然排序與Comparator(比較器)可以參考我在介紹集合Set時的講解。
注意:隊列的頭是按指定排序方式的最小元素。如果多個元素都是最小值,則頭是其中一個元素——選擇方法是任意的。
注意:當PriorityQueue中沒有指定Comparator時,加入PriorityQueue的元素必須實現了Comparable接口(即元素是可比較的),否則會導致 ClassCastException。
下面具體寫個例子來展示PriorityQueue中的排序方式:
PriorityQueue<Integer> qi = new PriorityQueue<Integer>();
qi.add(5);
qi.add(2);
qi.add(1);
qi.add(10);
qi.add(3);
while (!qi.isEmpty()){
System.out.print(qi.poll() + ",");
}
System.out.println();
//采用降序排列的方式,越小的越排在隊尾
Comparator<Integer> cmp = new Comparator<Integer>() {
public int compare(Integer e1, Integer e2) {
return e2 - e1;
}
};
PriorityQueue<Integer> q2 = new PriorityQueue<Integer>(5,cmp);
q2.add(2);
q2.add(8);
q2.add(9);
q2.add(1);
while (!q2.isEmpty()){
System.out.print(q2.poll() + ",");
}
輸出結果:
1,2,3,5,10,
9,8,2,1,
由此可以看出,默認情況下PriorityQueue采用自然排序。指定Comparator的情況下,PriorityQueue采用指定的排序方式。
PriorityQueue的方法
PriorityQueue實現了Queue接口,下面列舉出PriorityQueue的方法。
PriorityQueue的本質
PriorityQueue 本質也是一個動態數組,在這一方面與ArrayList是一致的。
PriorityQueue調用默認的構造方法時,使用默認的初始容量(DEFAULT_INITIAL_CAPACITY=11
)創建一個 PriorityQueue,并根據其自然順序來排序其元素(使用加入其中的集合元素實現的Comparable)。
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
當使用指定容量的構造方法時,使用指定的初始容量創建一個 PriorityQueue,并根據其自然順序來排序其元素(使用加入其中的集合元素實現的Comparable)。
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
當使用指定的初始容量創建一個 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;
}
從第三個構造方法可以看出,內部維護了一個動態數組。當添加元素到集合時,會先檢查數組是否還有余量,有余量則把新元素加入集合,沒余量則調用 grow()
方法增加容量,然后調用siftUp
將新加入的元素排序插入對應位置。
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
除此之外,還要注意:
①PriorityQueue不是線程安全的。如果多個線程中的任意線程從結構上修改了列表, 則這些線程不應同時訪問 PriorityQueue 實例,這時請使用線程安全的PriorityBlockingQueue 類。
②不允許插入 null 元素。
③PriorityQueue實現插入方法(offer、poll、remove() 和 add 方法) 的時間復雜度是O(log(n)) ;實現 remove(Object) 和 contains(Object) 方法的時間復雜度是O(n) ;實現檢索方法(peek、element 和 size)的時間復雜度是O(1)。所以在遍歷時,若不需要刪除元素,則以peek的方式遍歷每個元素。
④方法iterator()中提供的迭代器并不保證以有序的方式遍歷優PriorityQueue中的元素。
Dueue接口與ArrayDeque實現類
Dueue接口
Deque接口是Queue接口的子接口,它代表一個雙端隊列。LinkedList也實現了Deque接口,所以也可以被當作雙端隊列使用。也可以看到前面對LinkedList的介紹來理解Deque接口。
因此Deque接口增加了一些關于雙端隊列操作的方法。
void addFirst(E e):將指定元素插入此列表的開頭。
void addLast(E e): 將指定元素添加到此列表的結尾。
E getFirst(E e): 返回此列表的第一個元素。
E getLast(E e): 返回此列表的最后一個元素。
boolean offerFirst(E e): 在此列表的開頭插入指定的元素。
boolean offerLast(E e): 在此列表末尾插入指定的元素。
E peekFirst(E e): 獲取但不移除此列表的第一個元素;如果此列表為空,則返回 null。
E peekLast(E e): 獲取但不移除此列表的最后一個元素;如果此列表為空,則返回 null。
E pollFirst(E e): 獲取并移除此列表的第一個元素;如果此列表為空,則返回 null。
E pollLast(E e): 獲取并移除此列表的最后一個元素;如果此列表為空,則返回 null。
E removeFirst(E e): 移除并返回此列表的第一個元素。
boolean removeFirstOccurrence(Objcet o): 從此列表中移除第一次出現的指定元素(從頭部到尾部遍歷列表時)。
E removeLast(E e): 移除并返回此列表的最后一個元素。
boolean removeLastOccurrence(Objcet o): 從此列表中移除最后一次出現的指定元素(從頭部到尾部遍歷列表時)。
從上面方法中可以看出,Deque不僅可以當成雙端隊列使用,而且可以被當成棧來使用,因為該類里還包含了pop(出棧)、push(入棧)兩個方法。
Deque與Queue、Stack的關系
當 Deque 當做 Queue隊列使用時(FIFO),添加元素是添加到隊尾,刪除時刪除的是頭部元素。從 Queue 接口繼承的方法對應Deque 的方法如圖所示:
Deque 也能當Stack棧用(LIFO)。這時入棧、出棧元素都是在 雙端隊列的頭部 進行。Deque 中和Stack對應的方法如圖所示:
注意:Stack過于古老,并且實現地非常不好,因此現在基本已經不用了,可以直接用Deque來代替Stack進行棧操作。
ArrayDeque
顧名思義,就是用數組實現的Deque;既然是底層是數組那肯定也可以指定其capacity,也可以不指定,默認長度是16,然后根據添加的元素的個數,動態擴展。ArrayDeque由于是兩端隊列,所以其順序是按照元素插入數組中對應位置產生的(下面會具體說明)。
由于本身數據結構的限制,ArrayDeque沒有像ArrayList中的trimToSize方法可以為自己瘦身。ArrayDeque的使用方法就是上面的Deque的使用方法,基本沒有對Deque拓展什么方法。
ArrayDeque的本質
循環數組
ArrayDeque為了滿足可以同時在數組兩端插入或刪除元素的需求,其內部的動態數組還必須是循環的,即循環數組(circular array),也就是說數組的任何一點都可能被看作起點或者終點。
ArrayDeque維護了兩個變量,表示ArrayDeque的頭和尾
transient int head;
transient int tail;
當向頭部插入元素時,head下標減一然后插入元素。而 tail表示的索引為當前末尾元素表示的索引值加一。若當向尾部插入元素時,直接向tail表示的位置插入,然后tail再減一。
具體以下面的圖片為例解釋。
在上圖中:左邊圖表示從頭部插入了4個元素,尾部插入了2個。初始的時候,head=0,tail=0。當從頭部插入元素5,head-1,由于數組是循環數組,則移動到數組的最后位置插入5。當從頭部插入元素34,head-1然后再對應位置插入。下面以此類推,最后在頭部插入4個元素。當在尾部插入12時,直接在0的位置插入,然后tail=tail+1=1,當從尾部插入7時,直接在1的位置插入,然后tail = tail +1=2。最后隊列中的輸出順序是8,3,34,5, 12, 7。
把數組看成一個首尾相接的圓形數組更好理解循環數組的含義。
下面具體看看ArrayDeque怎么把循環數組實際應用的?
addFirst(E e)
為例來研究
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
當加入元素時,先看是否為空(ArrayDeque不可以存取null元素,因為系統根據某個位置是否為null來判斷元素的存在)。然后head-1插入元素。head = (head - 1) & (elements.length - 1)
很好的解決了下標越界的問題。這段代碼相當于取模,同時解決了head為負值的情況。因為elements.length必需是2的指數倍(代碼中有具體操作),elements - 1就是二進制低位全1,跟head - 1相與之后就起到了取模的作用。如果head - 1為負數,其實只可能是-1,當為-1時,和elements.length - 1進行與操作,這時結果為elements.length - 1。其他情況則不變,等于它本身。
當插入元素后,在進行判斷是否還有余量。因為tail總是指向下一個可插入的空位,也就意味著elements數組至少有一個空位,所以插入元素的時候不用考慮空間問題。
下面再說說擴容函數doubleCapacity(),其邏輯是申請一個更大的數組(原數組的兩倍),然后將原數組復制過去。過程如下圖所示:
圖中我們看到,復制分兩次進行,第一次復制head右邊的元素,第二次復制head左邊的元素。
//doubleCapacity()
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // head右邊元素的個數
int newCapacity = n << 1;//原空間的2倍
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 = (E[])a;
head = 0;
tail = n;
}
由此,我們便理解了ArrayDeque循環數組添加以及擴容的過程,其他操作類似。
注意:ArrayDeque不是線程安全的。 當作為棧使用時,性能比Stack好;當作為隊列使用時,性能比LinkedList好。
以上就是關于集合Queue的介紹。
由淺入深理解java集合(一)——集合框架 Collction、Map
由淺入深理解java集合(二)——集合 Set
由淺入深理解java集合(三)——集合 List
由淺入深理解java集合(五)——集合 Map
由淺入深理解java集合(六)——集合增刪改查的細節、性能及選擇推薦(待更新)