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、插入、刪除、獲取操作支持兩種形式:快速失敗和返回null或true/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本質是一個動態數組,默認實現由三種構造方法:
- public PriorityQueue() 調用無參構造方法時,使用默認的初始容量(
DEFAULT_INITIAL_CAPACITY=11
)來創建PriorityQueue,並根據其自然順序排序其中的元素(排序方式使用的是元素中實現的Comparable接口); - public PriorityQueue(int initialCapacity) 調用指定容量構造方法時,使用initialCapacity定義初始容量創建PriorityQueue,並根據其自然順序排序其中的元素(排序方式使用的是元素中實現的Comparable接口);
- 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)方法比較后比較大的的往后排。這種方式就是自然排序。