由淺入深理解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的介紹。

由淺入深理解java集合(一)——集合框架 Collction、Map
由淺入深理解java集合(二)——集合 Set
由淺入深理解java集合(三)——集合 List
由淺入深理解java集合(五)——集合 Map
由淺入深理解java集合(六)——集合增刪改查的細節、性能及選擇推薦(待更新)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容