由淺入深理解java集合(四)——集合-Queue

今天我們來介紹下集合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,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);
    }

當使用指定容量的構造方法時,使用指定的初始容量創建一個 PriorityQueue,並根據其自然順序來排序其元素(使用加入其中的集合元素實現的Comparable)。

 public PriorityQueue(int initialCapacity) {
        this(initialCapacity, null);
    }

當使用指定的初始容量創建一個 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;
    }

從第三個構造方法可以看出,內部維護了一個動態數組。當添加元素到集合時,會先檢查數組是否還有餘量,有餘量則把新元素加入集合,沒餘量則調用 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;
    }

除此之外,還要注意:
①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;

當向頭部插入元素時,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();
    }

當加入元素時,先看是否爲空(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;
}

由此,我們便理解了ArrayDeque循環數組添加以及擴容的過程,其他操作類似。
注意: ArrayDeque不是線程安全的。 當作爲棧使用時,性能比Stack好;當作爲隊列使用時,性能比LinkedList好。

以上就是關於集合Queue的介紹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章