Java中的集合(三)繼承Collection的Queue接口


Java中的集合(三)繼承Collection的Queue接口

一、Queue介紹

Queue接口繼承自Collection接口,是Java中定義的一種隊列數據結構,元素是有序的(按插入順序排序),先進先出(FIFO)原則。不支持隨機訪問數據,新元素插入(offer)到隊列的尾部,訪問元素(poll)操作會返回隊列頭部的元素。通常,隊列不允許隨機訪問隊列中的元素。

隊列:是計算機中的一種數據結構,保存在其中的數據具有“先進先出(FIFO,First In First Out)”的特性,新元素插入(offer)到隊列的尾部,訪問元素(poll)操作會返回隊列頭部的元素。通常,隊列不允許隨機訪問隊列中的元素。

在Java中,隊列分為2種形式,一種是單隊列,一種是循環隊列;

  通常,都是使用數組來實現隊列,假定數組的長度為5,也就是隊列的長度為5;

  (一)、單隊列:

1、創建一個長度為5的空數組,定義兩個屬性front、rear,分別代表着頭指針和尾指針。

2、向數組中插入數據

 

3、移除頭部元素:元素1、元素2

 

4、再向數組中插入數據,此時rear指向一個不存在的下標

 

此時,數組就會出現“假溢出”的現象,尾指針指向了一個不存在的下標,如果要解決這種情況,一般有兩種方法:

1、無限擴充數組容量;

2、使用循環隊列。

(二)、循環隊列

當尾指針指向了一個不存在的下標時,即超過數組大小時,此時我們判斷數組頭部是否有空余的空間,如果有就把尾指針指向頭部空余的空間,如下圖:

 

循環隊列就是將單隊列的頭尾相連,形成一個圓形,這樣就不會出現下標溢出的現象(distruptor實現)。 

二、Queue類圖

1、Queue接口繼承自Collection接口;

2、Queue接口分別有Deque子接口和AbstractQueue抽象類;

3、Deque子接口分別有LinkedList類和ArrayDeque類;

4、AbstractQueue抽象類有PriorityQueue實現類

三、Deque(Double-ended queue)接口

Deque接口是Queue接口的子接口,創建了雙端隊列結構,靈活性更強,可以前向或后向迭代,在隊頭隊尾均可插入或刪除元素的線性集合。它的兩個主要實現類是ArrayDeque和LinkedList

Deque接口支持容量固定的雙端隊列,也支持容量不固定的雙端隊列,一般情況下,雙端隊列的容量是不固定的。

(一)、特點

1、插入、刪除、獲取操作支持兩種形式:快速失敗和返回nulltrue/false;

2、既具有FIFO(First in, First out,先進先出)特性,又具有LIFO(Last in, First out,后進先出)特性;

3、不推薦插入null,因為null作為返回值表示隊列為空;

4、未定義基於元素的equals和hashCode;

5、不支持索引訪問元素;

6、Deque不僅是雙端隊列,還可以當做棧來使用,因為該類定義了pop(出站),push(入棧)等方法。

(二)、Deque接口與Queue接口、Stack的關系

從上面描述可以知道,Deque不僅可以當做雙端隊列使用,還可以當做棧來使用。

1、Deque當做雙端隊列使用時,Deque接口與Queue接口的關系

當 Deque 當做 Queue隊列使用時(FIFO),添加元素是添加到隊尾,刪除時刪除的是頭部元素。從 Queue 接口繼承的方法對應Deque 的方法如圖所示:

2、Deque當做棧使用時,Deque接口與Stack的關系

當Deque 當做Stack棧用(LIFO)。這時入棧、出棧元素都是在 雙端隊列的頭部 進行。Deque 中和Stack對應的方法如圖所示:


注意:由於Stack比較古老,功能實現非常不友好,現在已經基本不適用程序開發,因此可以選擇Deque接口代替Stack進行棧操作。 

四、ArrayDeque實現類

Deque接口下分別有兩個實現類,分別為ArrayDeque和LinkedList,LinkedList在本節暫不講述,這節主要講述ArrayDeque。

(一)、ArrayDeque:基於循環數組實現的線性雙端隊列,大小可變,不允許null。

1、底層是通過數組實現的,數組大小默認是16,可以指定長度,也可不指定,根據添加元素的個數,動態擴容數組容量。可以為了滿足可以同時插入和刪除元素,需要該數組必須是循環數組,也就是數組中的每一個點都可以看成是起點或者終點。

2、ArrayDeque是線程不安全的,在多線程環境下,需要手動同步;另外,ArrayDeque不允許插入null元素。

3、由於ArrayDeque是基於頭尾指針來實現Deque的,所以不能直接訪問第一個和最后一個元素,如果想要遍歷元素,需要使用Iterator迭代器,可以使用正反迭代遍歷元素。

4、ArrayDeque一般優於鏈表隊列/雙端隊列,有限數量的垃圾產生(舊數組將被丟棄在擴展),建議使用deque,ArrayDeque優先。

(二)、ArrayDeque操作簡圖

假定數組的長度為6,也就是ArrayDeque隊列的長度為6;


 

從上面圖中可以看出:front總是指向數組中的第一個有效元素的位置,rear終是指向第一個可以可以插入元素的空間位置,所以front並一定等於0,且不總會比rear大,rear也不總是比front小。

(三)、ArrayDeque中,循環數組的實現

ArrayDeque維護了兩個屬性,分別指向頭指針和尾指針:

 transient int head; // 指向頭指針 

 transient int tail; // 指向尾指針 

假定數組的長度為10,也就是ArrayDeque隊列的長度為10;

1、ArrayDeque剛創建時;

2、當向尾部插入時,直接在tail下標的位置插入元素,所以tail下標 + 1,head小標不變;


3、當從頭部插入時,head下標 - 1,然后插入元素,tail下標為當前數組末尾元素的下標不變;

通過上面的步驟可以知道,將ArrayDeque看出成是一個首尾相接的圓形數組更好理解循環數組的含義。 

通過addFrist(E e)代碼,看看ArrayQueue是如何實現的:

1 public void addFirst(E e) {
2     if (e == null)
3         throw new NullPointerException();
4 
5     elements[head = (head - 1) & (elements.length - 1)] = e;
6     if (head == tail)
7         doubleCapacity();
8 }        
    • 當加入元素時,先看是否為空(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()

擴容函數doubleCapacity()的邏輯是:申請一個更大容量的數組,將原數組原樣復制到新數組中。


從上圖可以看出,復制分為兩次進行:先復制head右邊的元素,然后再復制head左邊的元素。

 1 private void doubleCapacity() {
 2     assert head == tail;
 3     int p = head;
 4     int n = elements.length;
 5     int r = n - p; // head右邊元素的個數
 6     int newCapacity = n << 1;//原空間的2倍
 7     if (newCapacity < 0)
 8         throw new IllegalStateException("Sorry, deque too big");
 9     Object[] a = new Object[newCapacity];
10     System.arraycopy(elements, p, a, 0, r);//復制右半部分,對應上圖中綠色部分
11     System.arraycopy(elements, 0, a, r, p);//復制左半部分,對應上圖中灰色部分
12     elements = (E[])a;
13     head = 0;
14     tail = n;
15 }

(五)、小結

通過上面描述,我們便理解了ArrayDeque循環數組添加以及擴容的過程,另外需要注意的是:ArrayDeque不是線程安全的。 當作為棧使用時,性能比Stack好;當作為隊列使用時,性能比LinkedList好。

五、PriorityQueue實現類

(一)、PriorityQueue:底層基於數組實現的堆結構的優先隊列。

 PriorityQueue是AbstractQueue的子類,AbstractQueue又實現了Queue接口,所以PriorityQueue具有Queue接口的優先隊列。

 優先隊列與普通隊列不同,普通隊列遵循“FIFO”的特性,獲取元素時根據元素的插入順序獲取,優先隊列獲取元素時根據元素的優先級,獲取優先級最高的數據。

(二)、PriorityQueue的排序方式

PriorityQueue保存隊列元素時不是按照插入隊列順序進行排序,而是按照插入元素的大小進行排序的。因此當調用peek()、pop()方法時,不是取出最先插入的元素,而是取出隊列當中最小元素。

1、排序的方式

PriorityQueue隊列當中的元素是可以默認自然排序(數值型元素默認是最小的在隊列頭部,字符串則按字典序排序),或者通過Comparator(比較器)在隊列實例化指定排序方式。

注意:當PriorityQueue中沒有指定Comparator時,加入PriorityQueue的元素必須實現了Comparable接口(即元素是可比較的),否則會導致 ClassCastException。

(三)、PriorityQueue的本質

PriorityQueue本質是一個動態數組,默認實現由三種構造方法:

  1.  public PriorityQueue() 調用無參構造方法時,使用默認的初始容量(DEFAULT_INITIAL_CAPACITY=11)來創建PriorityQueue,並根據其自然順序排序其中的元素(排序方式使用的是元素中實現的Comparable接口);
  2.  public PriorityQueue(int initialCapacity) 調用指定容量構造方法時,使用initialCapacity定義初始容量創建PriorityQueue,並根據其自然順序排序其中的元素(排序方式使用的是元素中實現的Comparable接口);
  3.  PriorityQueue(int initialCapacity,Comparator<? super E> comparator) 當使用指定的初始容量創建一個 PriorityQueue,並根據指定的比較器comparator來排序其元素。

從上面的三個構造函數可以得出:PriorityQueue內部維護了一個動態數組,

除此之外,還要注意:

    • 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中的元素。

(四)、自然排序和Comparator比較器

1、Java中的兩種比較器:Conparator和Comparable

Comparable和Comparator接口都是為了對類進行比較,眾所周知,諸如int,double等基本數據類型,Java可以對他們進行比較,而對於對象即類的比較,需要人工定義比較用到的字段比較邏輯。可以把Comparable理解為內部比較器,而Comparator是外部比較器

(1)、Comparable接口:內部比較器,實現了Comparable接口的類需要實現compareTo(T o)方法,傳入一個外部參數進行比對;

當一個對象調用該compareTo(T o)方法與另一個對象比較時,例如o1.compareTo(o2)

  • 如果該方法返回0,則表明兩個對象相等;
  • 如果該方法返回一個整數,則表明o1大於o2;
  • 如果該方法返回一個負整數,則表明o1小於o2。

(2)、Conparator接口:外部比較器,實現了Comparator接口的方法需要實現compare(T o1,T o2)方法,對外部傳入的兩個類進行比較,從而讓外部方法在比較時調用;

該compare(T o1,T o2)方法用於比較o1,o2的大小:

  • 如果該方法返回正整數,則表明o1大於o2;
  • 如果該方法返回0,則表明o1等於o2;
  • 如果該方法返回負整數,則表明o1小於o2。

2、自然排序

自然排序是調用元素所屬類的compareTo(T o)方法來比較元素之間的大小關系,然后將集合元素按升序排列,即把通過compareTo(T o)方法比較后比較大的的往后排。這種方式就是自然排序。 


免責聲明!

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



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