由淺入深理解java集合(四)
今天我們來介紹下集合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
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
輸出結果:
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);
}
- 1
- 2
- 3
- 1
- 2
- 3
當使用指定容量的構造方法時,使用指定的初始容量建立一個 PriorityQueue,並根據其自然順序來排序其元素(使用加入其中的集合元素實現的Comparable)。
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
- 1
- 2
- 3
- 1
- 2
- 3
當使用指定的初始容量建立一個 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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
從第三個構造方法可以看出,內部維護了一個動態陣列。當新增元素到集合時,會先檢查陣列是否還有餘量,有餘量則把新元素加入集合,沒餘量則呼叫 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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
除此之外,還要注意:
①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;
- 1
- 2
- 1
- 2
當向頭部插入元素時,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();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
當加入元素時,先看是否為空(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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
由此,我們便理解了ArrayDeque迴圈陣列新增以及擴容的過程,其他操作類似。
注意: ArrayDeque不是執行緒安全的。 當作為棧使用時,效能比Stack好;當作為佇列使用時,效能比LinkedList好。
以上就是關於集合Queue的介紹。