Java雙向佇列Deque棧與佇列
Java中實際上提供了java.util.Stack
來實現棧結構,但官方目前已不推薦使用,而是使用java.util.Deque
雙端佇列來實現佇列與棧的各種需求.如下圖所示java.util.Deque
的實現子類有java.util.LinkedList
和java.util.ArrayDeque
.顧名思義前者是基於連結串列,後者基於資料實現的雙端佇列.
總體介紹
要講棧和佇列,首先要講Deque介面。Deque的含義是“double ended queue”,即雙端佇列,它既可以當作棧使用,也可以當作佇列使用。下表列出了Deque與Queue相對應的介面:
下表列出了Deque與Stack對應的介面:
上面兩個表共定義了Deque的12個介面。新增,刪除,取值都有兩套介面,它們功能相同,區別是對失敗情況的處理不同。一套介面遇到失敗就會丟擲異常,另一套遇到失敗會返回特殊值(false或null)。除非某種實現對容量有限制,大多數情況下,新增操作是不會失敗的。雖然Deque的介面有12個之多,但無非就是對容器的兩端進行操作,或新增,或刪除,或檢視。明白了這一點講解起來就會非常簡單。
ArrayDeque
從名字可以看出ArrayDeque底層通過陣列實現,為了滿足可以同時在陣列兩端插入或刪除元素的需求,該陣列還必須是迴圈的,即迴圈陣列(circular array),也就是說陣列的任何一點都可能被看作起點或者終點。ArrayDeque是非執行緒安全
上圖中我們看到,head指向首端第一個有效元素,tail指向尾端第一個可以插入元素的空位。因為是迴圈陣列,所以head不一定總等於0,tail也不一定總是比head大。
addFirst()
針對首端插入實際需要考慮:1.空間是否夠用,以及2.下標是否越界的問題。上圖中,如果head為0之後接著呼叫addFirst(),雖然空餘空間還夠用,但head為-1,下標越界了。下列程式碼很好的解決了這兩個問題。
public void addFirst(E e) { if (e == null) throw new NullPointerException(); //下標越界問題解決方案 elements[head = (head - 1) & (elements.length - 1)] = e; //容量問題解決方案 if (head == tail) 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左邊的元素。
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;
}
addLast()
addLast(E e)的作用是在Deque的尾端插入元素,也就是在tail的位置插入元素,由於tail總是指向下一個可以插入的空位,因此只需要elements[tail] = e;即可。插入完成後再檢查空間,如果空間已經用光,則呼叫doubleCapacity()進行擴容。與first比較類似就不多分析了.
其他操作也是差不多的方式,唯一麻煩的head與tail位置轉換也用取餘巧妙的化解了.
LinkedList
LinkedList
實現了Deque
介面,因此其具備雙端佇列的特性,由於其是連結串列結構,因此不像ArrayDeque
要考慮越界問題,容量問題,那麼對應操作就很簡單了,另外當需要使用棧和佇列是官方推薦的是ArrayDeque
,因此這裡不做多的分析.