1. 程式人生 > >Java高階技術第四章——Java容器類Queue之體驗雙端佇列ArrayQueue設計之妙

Java高階技術第四章——Java容器類Queue之體驗雙端佇列ArrayQueue設計之妙

前言

ArrayDeque

ArrayDeque的資料結構要比PriorityQueue要簡單得多,是通過陣列來實現的。但是,ArrayDeque的特點是一個雙端佇列,既可以實現FIFO的Queue,也可以實現LIFO的Stack.
ArrayDeque雖然原理比較簡單,但是其精髓是使用了位運算來加快計算效率。它的原始碼如下:

  1. 採用陣列儲存資料,陣列的預設長度是16:
    /**
     * Constructs an empty array deque with an initial capacity
     * sufficient to hold 16 elements.
     */
public ArrayDeque() { elements = new Object[16]; }
  1. 儲存陣列長度一定是2的冪指數,且最小為8,其通過這樣的位運算來實現的:
 /**
     * 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;

    // ******  Array allocation and resizing utilities ******
private static int calculateSize(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 } return initialCapacity; } public ArrayDeque(int numElements) { allocateElements(numElements); } private void allocateElements(int numElements) { elements = new Object[calculateSize(numElements)]; }

從上面的程式碼可以看出來,使用calculateSize()方法可以自動計算出合適的2的n次冪的數值,用下面的程式碼作一個測試:

public class Main {

    private static int getCapacity(int numElements) {
        int 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
        return initialCapacity;
    }

    public static void main(String[] args){
        for(int i=0;i<5;i++){
            System.out.println(i + " " + getCapacity(i));
        }
    }
}

輸出結果是:

0 1
1 2
2 4
3 4
4 8

  1. 獲取佇列長度
    ArrayDeque有兩個變數head,tail分別代表陣列中隊首和隊尾的索引,由於是雙端佇列,實現起來類似一個迴圈陣列,有可能隊尾的索引數值比隊頭的數值還要小,其是通過這種方法來實現獲取陣列長度的:
    public int size() {
        return (tail - head) & (elements.length - 1);
    }

依然使用了位運算,之所以儲存陣列的長度都是2的n次冪的數值,是因為:

2^n - 1

計算之後的二進位制結果前半部分都是0,後半部分都是1,能夠起到類似掩碼的作用,
例如下面的示意圖:

實際上兼取絕對值與取模於一體了。

  1. 清空佇列
    public void clear() {
        int h = head;
        int t = tail;
        if (h != t) { // clear all cells
            head = tail = 0;
            int i = h;
            int mask = elements.length - 1;
            do {
                elements[i] = null;
                i = (i + 1) & mask;
            } while (i != t);
        }
    }

清空佇列並不將陣列的陣列元素數量再次置為初始值,只是將陣列中對每個元素的引用置為null,這樣可以被垃圾回收掉。
可以看到,在遍歷佇列中每個元素的時候,巧妙地運用了位運算,仍然將元素數量減1作為掩碼使用。

  1. 新增元素
    新增元素的核心原始碼如下:
    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
    }

判斷陣列容量不夠時,需要進行擴容,判斷方式依然是使用位運算來完成的。
對於迴圈陣列中判斷是否達到最大容量的一種方法就是判斷tail與head是否相等,如果相等,那麼首位相交,達到最大容量。
但是,在起始位置時候,head與tail也可能會相等。那麼,可以將tail指向下一個元素將要插入的位置,那麼當:

(tail + 1) % array.length == head

此時,也就是下一個將要插入的位置與head索引相同,那麼證明這個陣列已經被填充滿了,需要擴容了。
在上面的原始碼中,使用了掩碼的位運算方式來代替取模的方式,效率更高。可見,陣列長度是2的n次冪數值用處很大。

  1. 陣列擴容
    在插入元素之後,要進行陣列是否擴容的判斷,在如果需要擴容,則擴容一倍,確保儲存陣列的長度仍然是2的n次冪數值。
    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;
    }