前言
Java裡有一個叫做Stack的類,卻沒有叫做Queue的類(它是個介面名字)。當需要使用棧時,Java已不推薦使用Stack,而是推薦使用更高效的ArrayDeque;既然Queue只是一個介面,當需要使用佇列時也就首選ArrayDeque了(次選是LinkedList)。
總體介紹
要講棧和佇列,首先要講Deque介面。Deque的含義是“double ended queue”,即雙端佇列,它既可以當作棧使用,也可以當作佇列使用。下表列出了Deque與Queue相對應的介面:
Queue Method | Equivalent Deque Method | 說明 |
---|---|---|
add(e) |
addLast(e) |
向隊尾插入元素,失敗則丟擲異常 |
offer(e) |
offerLast(e) |
向隊尾插入元素,失敗則返回false |
remove() |
removeFirst() |
獲取並刪除隊首元素,失敗則丟擲異常 |
poll() |
pollFirst() |
獲取並刪除隊首元素,失敗則返回null |
element() |
getFirst() |
獲取但不刪除隊首元素,失敗則丟擲異常 |
peek() |
peekFirst() |
獲取但不刪除隊首元素,失敗則返回null |
下表列出了Deque與Stack對應的介面:
Stack Method | Equivalent Deque Method | 說明 |
---|---|---|
push(e) |
addFirst(e) |
向棧頂插入元素,失敗則丟擲異常 |
無 | offerFirst(e) |
向棧頂插入元素,失敗則返回false |
pop() |
removeFirst() |
獲取並刪除棧頂元素,失敗則丟擲異常 |
無 | pollFirst() |
獲取並刪除棧頂元素,失敗則返回null |
peek() |
peekFirst() |
獲取但不刪除棧頂元素,失敗則丟擲異常 |
無 | peekFirst() |
獲取但不刪除棧頂元素,失敗則返回null |
上面兩個表共定義了Deque的12個介面。新增,刪除,取值都有兩套介面,它們功能相同,區別是對失敗情況的處理不同。一套介面遇到失敗就會丟擲異常,另一套遇到失敗會返回特殊值(false
或null
)。除非某種實現對容量有限制,大多數情況下,新增操作是不會失敗的。雖然Deque的介面有12個之多,但無非就是對容器的兩端進行操作,或新增,或刪除,或檢視。明白了這一點講解起來就會非常簡單。
ArrayDeque和LinkedList是Deque的兩個通用實現,由於官方更推薦使用AarryDeque用作棧和佇列,加之上一篇已經講解過LinkedList,本文將著重講解ArrayDeque的具體實現。
從名字可以看出ArrayDeque底層通過陣列實現,為了滿足可以同時在陣列兩端插入或刪除元素的需求,該陣列還必須是迴圈的,即迴圈陣列(circular array),也就是說陣列的任何一點都可能被看作起點或者終點。ArrayDeque是非執行緒安全的(not thread-safe),當多個執行緒同時使用的時候,需要程式設計師手動同步;另外,該容器不允許放入null
元素。
上圖中我們看到,head
指向首端第一個有效元素,tail
指向尾端第一個可以插入元素的空位。因為是迴圈陣列,所以head
不一定總等於0,tail
也不一定總是比head
大。
方法剖析
addFirst()
addFirst(E e)
的作用是在Deque的首端插入元素,也就是在head
的前面插入元素,在空間足夠且下標沒有越界的情況下,只需要將elements[--head] = e
即可。
實際需要考慮:1.空間是否夠用,以及2.下標是否越界的問題。上圖中,如果head
為0
之後接著呼叫addFirst()
,雖然空餘空間還夠用,但head
為-1
,下標越界了。下列程式碼很好的解決了這兩個問題。
1 2 3 4 5 6 7 8 |
//addFirst(E e) public void addFirst(E e) { if (e == null)//不允許放入null throw new NullPointerException(); elements[head = (head - 1) & (elements.length - 1)] = e;//2.下標是否越界 if (head == tail)//1.空間是否夠用 doubleCapacity();//擴容 } |
上述程式碼我們看到,空間問題是在插入之後解決的,因為tail
總是指向下一個可插入的空位,也就意味著elements
陣列至少有一個空位,所以插入元素的時候不用考慮空間問題。
下標越界的處理解決起來非常簡單,head = (head - 1) & (elements.length - 1)
就可以了,這段程式碼相當於取餘,同時解決了head
為負值的情況。因為elements.length
必需是2
的指數倍,elements - 1
就是二進位制低位全1
,跟head - 1
相與之後就起到了取模的作用,如果head - 1
為負數(其實只可能是-1),則相當於對其取相對於elements.length
的補碼。
下面再說說擴容函式doubleCapacity()
,其邏輯是申請一個更大的陣列(原陣列的兩倍),然後將原陣列複製過去。過程如下圖所示:
圖中我們看到,複製分兩次進行,第一次複製head
右邊的元素,第二次複製head
左邊的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//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; } |
addLast()
addLast(E e)
的作用是在Deque的尾端插入元素,也就是在tail
的位置插入元素,由於tail
總是指向下一個可以插入的空位,因此只需要elements[tail] = e;
即可。插入完成後再檢查空間,如果空間已經用光,則呼叫doubleCapacity()
進行擴容。
1 2 3 4 5 6 7 |
public void addLast(E e) { if (e == null)//不允許放入null throw new NullPointerException(); elements[tail] = e;//賦值 if ( (tail = (tail + 1) & (elements.length - 1)) == head)//下標越界處理 doubleCapacity();//擴容 } |
下標越界處理方式addFirt()
中已經講過,不再贅述。
pollFirst()
pollFirst()
的作用是刪除並返回Deque首端元素,也即是head
位置處的元素。如果容器不空,只需要直接返回elements[head]
即可,當然還需要處理下標的問題。由於ArrayDeque
中不允許放入null
,當elements[head] == null
時,意味著容器為空。
1 2 3 4 5 6 7 8 |
public E pollFirst() { E result = elements[head]; if (result == null)//null值意味著deque為空 return null; elements[h] = null;//let GC work head = (head + 1) & (elements.length - 1);//下標越界處理 return result; } |
pollLast()
pollLast()
的作用是刪除並返回Deque尾端元素,也即是tail
位置前面的那個元素。
1 2 3 4 5 6 7 8 9 |
public E pollLast() { int t = (tail - 1) & (elements.length - 1);//tail的上一個位置是最後一個元素 E result = elements[t]; if (result == null)//null值意味著deque為空 return null; elements[t] = null;//let GC work tail = t; return result; } |
peekFirst()
peekFirst()
的作用是返回但不刪除Deque首端元素,也即是head
位置處的元素,直接返回elements[head]
即可。
1 2 3 |
public E peekFirst() { return elements[head]; // elements[head] is null if deque empty } |
peekLast()
peekLast()
的作用是返回但不刪除Deque尾端元素,也即是tail
位置前面的那個元素。
1 2 3 |
public E peekLast() { return elements[(tail - 1) & (elements.length - 1)]; } |
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!