ArrayDeque詳解


美人如斯!

ArrayDeque是java中對雙端隊列的線性實現

一.特性

  1. 無容量大小限制,容量按需增長;
  2. 非線程安全隊列,無同步策略,不支持多線程安全訪問;
  3. 當用作棧時,性能優於Stack,當用於隊列時,性能優於LinkedList
  4. 兩端都可以操作
  5. 具有fail-fast特征
  6. 不能存儲null
  7. 支持雙向迭代器遍歷

注意: ArrayDeque的迭代器和大多數容器迭代器一樣,都是快速失敗(fail-fast),但是程序不能利用這個特性決定是或否進行了並發操作。

二.數據結構

為了更好的理解使用線性數組實現的雙端隊列,這里我們先來圖解線性數組實現基本數據結構-隊列:

如上圖所示,head指向隊頭,入隊加元素時,tail隊尾向后移動,出隊時從head出取出元素並移除,這樣就利用了線性數組實現先進先出的隊列數據結構,當head等於tail時,則表示隊列為空。
但是這樣存在問題:當不斷出隊時,head向后移動,前面空出來的空間就被浪費,導致不斷入隊時,需要數組擴容,出隊時造成大量空間無法使用,空間利用率低下!
假設,如果能將前面空出來的空間也利用起來進行存儲末尾的元素,則空間使用率將提高,這里就需要有個循環的思維,把這種線性的彎曲成一個圓環,這樣就可以反復使用空出來的空間,入隊時使用出隊空余出來的空間,就解決以上的問題,圖解如下:

同樣,當head等於tail時,則表示循環隊列為空。head和tail也是循環的,像鍾表中的時針,具有周期性。這里head和tail需要對長度lenth取模,這樣head和tail將一直在長度范圍內,可以作為數組的下標。

對於如何將數據分布到相應大小的連續空間中,常用的方式就是取模運算,即position=index%len,利用整數倍的周期性,將剩余的部分作為空間索引。

三.源碼分析

1. ArrayDeque數據域
/**
 * The array in which the elements of the deque are stored.
 * The capacity of the deque is the length of this array, which is
 * always a power of two. The array is never allowed to become
 * full, except transiently within an addX method where it is
 * resized (see doubleCapacity) immediately upon becoming full,
 * thus avoiding head and tail wrapping around to equal each
 * other.  We also guarantee that all array cells not holding
 * deque elements are always null.
 */
transient Object[] elements; // non-private to simplify nested class access

/**
 * The index of the element at the head of the deque (which is the
 * element that would be removed by remove() or pop()); or an
 * arbitrary number equal to tail if the deque is empty.
 */
transient int head;

/**
 * The index at which the next element would be added to the tail
 * of the deque (via addLast(E), add(E), or push(E)).
 */
transient int tail;

/**
 * The minimum capacity that we'll use for a newly created deque.
 * Must be a power of 2.
 */
private static final int MIN_INITIAL_CAPACITY = 8;

首先看下ArrayDeque持有的成員域,其中非常核心的是elements,head,tail三個。下面逐一介紹:

  • elements: 該數組用於存儲隊列元素,且是大小總是2的冪次方(后面會介紹為什么?)。這個數組不會滿容量,會在add方法中擴容,使得頭head和tail不會纏繞在一起(即head增長或不會超過tail,head減小時不會溢出到tail),這里隊列長度是2的冪次方的原因后續會闡明;
  • head: 雙端隊列的頭位置,出隊時或者彈出棧時的元素位置,加入雙端隊列頭端元素位置,表示當前頭元素位置;
  • tail:雙端隊列的尾,入隊和進棧時的元素位置,加入雙端隊列尾端的下個元素的索引,tail位總是空的;
  • MIN_INITIAL_CAPACITY:最小的初始化容量
2. 構造函數
/**
 * Constructs an empty array deque with an initial capacity
 * sufficient to hold 16 elements.
 */
public ArrayDeque() {
    elements = new Object[16];
}

/**
 * Constructs an empty array deque with an initial capacity
 * sufficient to hold the specified number of elements.
 *
 * @param numElements  lower bound on initial capacity of the deque
 */
public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

/**
 * Constructs a deque containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.  (The first element returned by the collection's
 * iterator becomes the first element, or <i>front</i> of the
 * deque.)
 *
 * @param c the collection whose elements are to be placed into the deque
 * @throws NullPointerException if the specified collection is null
 */
public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}
  • 第一個默認的無參構造函數:創建初始化大小為16的隊列
  • 第二個構造函數:根據參數numElements創建隊列,如果numElements小於8,則隊列初始化大小為8;如果numElements大於8,則初始化大小為大於numElements的最小2的冪次方。如:numElements=17,則初始化大小為32
  • 第三個構造函數:根據集合元素創建隊列,初始化大小為大於集合大小的最小2的冪次方

這里重點看下第二個構造器的過程。其中調用allocateElements(numElements)方法,該方法用來實現容量分配,下面看下內部具體實現:

/**
 * Allocates empty array to hold the given number of elements.
 *
 * @param numElements  the number of elements to hold
 */
private void allocateElements(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    elements = new Object[initialCapacity];
}

首先判斷指定大小numElements與MIN_INITIAL_CAPACITY的大小關系。如果小於MIN_INITIAL_CAPACITY,則直接分配大小為MIN_INITIAL_CAPACITY的數組;如果大於MIN_INITIAL_CAPACITY,則進行無符號右移操作,然后在加1,這樣就可以尋找到大於numElements的最小2的冪次方。
原理:無符號右移再進行按位或操作,就是將其低位全部補成1,然后再自加加一次,就是再向前進一位。這樣就能得到其最小的2次冪。之所以需要最多移16位,是為了能夠處理大於2^16次方數。

最后再判斷值是否小於0,因為如果初始值在int最大值231-1和230之間,進行一系列移位操作后將得到int最大值,再加1,則溢出變成負數,所以需要檢測臨界值,然后再右移1位!!!

接下來再來分析下ArrayDeque的幾個重要雙端操作。對於雙端隊列有哪些重要的雙端操作,可以移步至我的之前寫的另一篇文章Java中Deque特性及API

在詳細介紹ArrayDeque的重要API實現之前,以圖解的方式看下ArrayDeque構造函數初始化出的隊列的數據結構:

3. 重要行為

addFirst方法

/**
 * Inserts the specified element at the front of this deque.
 *
 * @param e the element to add
 * @throws NullPointerException if the specified element is null
 */
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

先用圖解的方式分析下這個方法,在第一次調用這個方法后,數據變化如下:

根據圖的變化來分析下代碼實現。
首先判斷插入元素是否為空,再計算即將插入的位置,計算出后將元素賦值給相應的槽位,最后再判斷隊列容量進行擴容。

  1. 將數組的高位端作為雙端隊列的頭部,將低位作為雙端隊列尾部。沒從頭部加入一個元素時,head頭逆時針向tail尾方向移動一個位置,實現上即將head減1后對數組的最大下標按位與運算。這里就利用了2的冪次方的特性,隊列容量設置為2的冪次方后,數組的最大下標位置等於2的冪次方減1,在二進制表示時,就是所有二進制位都是1。這樣head位置減1后與其進行按位與運算就能得到頭部插入的位置。

  2. 當head等於tail時,就表示隊列已經滿了。這時需要進行擴容。

下面再來看下擴容策略:

/**
 * Doubles the capacity of this deque.  Call only when full, i.e.,
 * when head and tail have wrapped around to become equal.
 */
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;
}
  1. 按照2倍方式擴容
  2. 擴容后,將原隊列中從頭部插入的元素即head右邊元素從擴容后新數組的0位置開始排放,然后將左邊的元素緊接着排放進新數組。
  3. 將head置0,tail置成擴容前數組長度。

如果從頭端插入,則head繼續逆時針旋轉方式插入新元素。從以上圖中不難看出addFirst是操作雙端隊列頭端,且是逆時針方式旋轉插入。接下來再看看從尾端插入的過程

addLast方法

/**
 * Inserts the specified element at the end of this deque.
 *
 * <p>This method is equivalent to {@link #add}.
 *
 * @param e the element to add
 * @throws NullPointerException if the specified element is null
 */
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

上述的addFirst是逆時針的插入方式,addLast剛好與其相反,即順時針方向插入,且tail表示的是下一個插入的元素的位置。

  1. 判斷元素是否為空,然后直接將元素插入tail槽位
  2. 然后tail向后移動一位,再按位與(控制循環)作為新的tail槽位
  3. 判斷新的tail槽位是否與head相等,然后依此進行擴容(這里擴容與上述擴容過程一樣,不再贅述)。

pollFirst方法

public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    // Element is null if deque empty
    if (result == null)
        return null;
    elements[h] = null;     // Must null out slot
    head = (h + 1) & (elements.length - 1);
    return result;
}
  1. 取出頭元素,如果頭元素為空,則返回null
  2. 否則,將頭元素槽位置為空(因為pollFirst是移除操作)
  3. 再將head順時針向后移動一位,即加1再和數組最大下標按位與計算出新的head

注:讀到這里,相信讀者已經已經對雙端隊列的數據結構已經非常清晰,即雙端操作的數組,tail向前(順時針)移動即從尾端插入元素或者向后移動即從尾端移除元素,head向后(逆時針)移動即從頭端插入元素或者向前移動即從頭端移除元素。這幾個過程正好具有FIFO和LIFO的特點,所以ArrayDeque既可以作為隊列Queue又可以作為棧Stack。

pollLast方法

public E pollLast() {
    int t = (tail - 1) & (elements.length - 1);
    @SuppressWarnings("unchecked")
    E result = (E) elements[t];
    if (result == null)
        return null;
    elements[t] = null;
    tail = t;
    return result;
}

從以上描述的ArrayDeque的數據結構和tail的含義中,可以大致思考下,從尾端移除元素的過程。

  1. 先將tail向后(逆時針)移動一位,然后對數組最大下標按位與計算出將要移除元素的槽位
  2. 取出計算出的槽位中元素,判斷是否為空,為空則返回null
  3. 如果不為空,則將該槽位置為空,將槽位下標作為新的tail

以上的過程基就是ArrayDeque的工作原理的最基本實現,其他的行為大都是基於這些過程實現:

offer方法:內部調用offerLast插入元素,返回插入結果true/false
add方法:內部調用addLast實現
poll方法:內部調用pollFirst實現
remove方法:內部調用removeFirst實現
peek方法:內部調用peekFirst實現
element方法:內部調用getFirst實現
pop方法:內部調用addFirst實現
push方法:內部調用removeFirst實現

這里不再詳述每個操作的具體實現,因為這些操作都是基於addFirst、addLast、pollFirst和pollLast實現。具體調用這些基礎行為實現的細節,讀者可以閱讀ArrayDeque源碼。

參考:

位運算總結(按位與,或,異或)
Java 中>>和>>>的區別
java int short long float double精度最大值整理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM