PriorityQueue是一種什么樣的容器呢?看過前面的幾個jdk容器分析的話,看到Queue這個單詞你一定會,哦~這是一種隊列。是的,PriorityQueue是一種隊列,但是它又是一種什么樣的隊列呢?它具有着什么樣的特點呢?它的底層實現方式又是怎么樣的呢?我們一起來看一下。
PriorityQueue其實是一個優先隊列,什么是優先隊列呢?這
和我們前面講的先進先出(First In First Out )的隊列的區別在於,優先隊列每次出隊的元素都是優先級最高的元素。那么怎么確定哪一個元素的優先級最高呢,jdk中使用堆這么一種數據結構,通過堆使得每次出隊的元素總是隊列里面最小的,而元素的大小比較方法可以由用戶指定,這里就相當於指定優先級嘍。
1.二叉堆介紹
那么堆又是什么一種數據結構呢、它有什么樣的特點呢?(以下見於百度百科)
(1)堆中某個節點的值總是不大於或不小於其父節點的值;
(2)堆總是一棵完全樹。
(2)堆總是一棵完全樹。
常見的堆有二叉堆、斐波那契堆等。而PriorityQueue使用的便是二叉堆,這里我們主要來分析和學習二叉堆。
二叉堆是一種特殊的堆,二叉堆是完全二叉樹或者是近似完全二叉樹。二叉堆有兩種:最大堆和最小堆。
最大堆:父結點的鍵值總是大於或等於任何一個子節點的鍵值;最小堆:父結點的鍵值總是小於或等於任何一個子節點的鍵值。
說到二叉樹我們就比較熟悉了,因為我們前面分析和學習過了二叉查找樹和紅黑樹(TreeMap)。慣例,我們以最小堆為例,用圖解來描述下什么是二叉堆。

上圖就是一顆完全二叉樹(二叉堆),我們可以看出什么特點嗎,那就是在第n層深度被填滿之前,不會開始填第n+1層深度,而且元素插入是從左往右填滿。
基於這個特點,二叉堆又可以用數組來表示而不是用鏈表,我們來看一下:

通過"用數組表示二叉堆"這張圖,我們可以看出什么規律嗎?那就是,
基於數組實現的二叉堆,對於數組中任意位置的n上元素,其左孩子在[
2n+1]
位置上,右孩子[2(n+1)]位置,它的父親則在[(n-1)/2]上,而根的位置則是[
0]。
好了、在了解了二叉堆的基本概念后,我們來看下jdk中PriorityQueue是怎么實現的。
2.PriorityQueue的底層實現
先來看下PriorityQueue的定義:
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {
我們看到PriorityQueue繼承了AbstractQueue抽象類,並實現了Serializable接口,AbstractQueue抽象類實現了Queue接口,對其中方法進行了一些通用的封裝,具體就不多看了。
下面再看下PriorityQueue的底層存儲相關定義:
1 // 默認初始化大小 2 privatestaticfinalintDEFAULT_INITIAL_CAPACITY = 11; 3 4 // 用數組實現的二叉堆,下面的英文注釋確認了我們前面的說法。 5 /** 6 * Priority queue represented as a balanced binary heap: the two 7 * children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The 8 * priority queue is ordered by comparator, or by the elements' 9 * natural ordering, if comparator is null: For each node n in the 10 * heap and each descendant d of n, n <= d. The element with the 11 * lowest value is in queue[0], assuming the queue is nonempty. 12 */ 13 private transient Object[] queue ; 14 15 // 隊列的元素數量 16 private int size = 0; 17 18 // 比較器 19 private final Comparator<? super E> comparator; 20 21 // 修改版本 22 private transient int modCount = 0;
我們看到jdk中的PriorityQueue的也是基於數組來實現一個二叉堆,並且注釋中解釋了我們前面的說法。
而Comparator這個比較器我們已經很熟悉了,我們說PriorityQueue是一個有限隊列,他可以由用戶指定優先級,就是靠這個比較器嘍。
3.PriorityQueue的構造方法
1 /** 2 * 默認構造方法,使用默認的初始大小來構造一個優先隊列,比較器comparator為空,這里要求入隊的元素必須實現Comparator接口 3 */ 4 public PriorityQueue() { 5 this(DEFAULT_INITIAL_CAPACITY, null); 6 } 7 8 /** 9 * 使用指定的初始大小來構造一個優先隊列,比較器comparator為空,這里要求入隊的元素必須實現Comparator接口 10 */ 11 public PriorityQueue( int initialCapacity) { 12 this(initialCapacity, null); 13 } 14 15 /** 16 * 使用指定的初始大小和比較器來構造一個優先隊列 17 */ 18 public PriorityQueue( int initialCapacity, 19 Comparator<? super E> comparator) { 20 // Note: This restriction of at least one is not actually needed, 21 // but continues for 1.5 compatibility 22 // 初始大小不允許小於1 23 if (initialCapacity < 1) 24 throw new IllegalArgumentException(); 25 // 使用指定初始大小創建數組 26 this.queue = new Object[initialCapacity]; 27 // 初始化比較器 28 this.comparator = comparator; 29 } 30 31 /** 32 * 構造一個指定Collection集合參數的優先隊列 33 */ 34 public PriorityQueue(Collection<? extends E> c) { 35 // 從集合c中初始化數據到隊列 36 initFromCollection(c); 37 // 如果集合c是包含比較器Comparator的(SortedSet/PriorityQueue),則使用集合c的比較器來初始化隊列的Comparator 38 if (c instanceof SortedSet) 39 comparator = (Comparator<? super E>) 40 ((SortedSet<? extends E>)c).comparator(); 41 else if (c instanceof PriorityQueue) 42 comparator = (Comparator<? super E>) 43 ((PriorityQueue<? extends E>)c).comparator(); 44 // 如果集合c沒有包含比較器,則默認比較器Comparator為空 45 else { 46 comparator = null; 47 // 調用heapify方法重新將數據調整為一個二叉堆 48 heapify(); 49 } 50 } 51 52 /** 53 * 構造一個指定PriorityQueue參數的優先隊列 54 */ 55 public PriorityQueue(PriorityQueue<? extends E> c) { 56 comparator = (Comparator<? super E>)c.comparator(); 57 initFromCollection(c); 58 } 59 60 /** 61 * 構造一個指定SortedSet參數的優先隊列 62 */ 63 public PriorityQueue(SortedSet<? extends E> c) { 64 comparator = (Comparator<? super E>)c.comparator(); 65 initFromCollection(c); 66 } 67 68 /** 69 * 從集合中初始化數據到隊列 70 */ 71 private void initFromCollection(Collection<? extends E> c) { 72 // 將集合Collection轉換為數組a 73 Object[] a = c.toArray(); 74 // If c.toArray incorrectly doesn't return Object[], copy it. 75 // 如果轉換后的數組a類型不是Object數組,則轉換為Object數組 76 if (a.getClass() != Object[].class) 77 a = Arrays. copyOf(a, a.length, Object[]. class); 78 // 將數組a賦值給隊列的底層數組queue 79 queue = a; 80 // 將隊列的元素個數設置為數組a的長度 81 size = a.length ; 82 }
構造方法還是比較容易理解的,第四個構造方法中,如果填入的集合c沒有包含比較器Comparator,則在調用initFromCollection初始化數據后,在調用heapify方法對數組進行調整,使得它符合二叉堆的規范或者特點,具體heapify是怎么構造二叉堆的,我們后面再看。
那么怎么樣調整才能使一些雜亂無章的數據變成一個符合二叉堆的規范的數據呢?
4.二叉堆的添加原理及PriorityQueue的入隊實現
我們回憶一下,我們在說紅黑樹TreeMap的時候說,紅黑樹為了維護其紅黑平衡,主要有三個動作:左旋、右旋、着色。那么二叉堆為了維護他的特點又需要進行什么樣的操作呢。
我們再來看下二叉堆(最小堆為例)的特點:
(1)父結點的鍵值總是小於或等於任何一個子節點的鍵值。
(2)
基於數組實現的二叉堆,對於數組中任意位置的n上元素,其左孩子在[
2n+1]
位置上,右孩子[2(n+1)]位置,它的父親則在[n-1/2]上,而根的位置則是[
0]。
為了維護這個特點,二叉堆在添加元素的時候,需要一個"上移"的動作,什么是"上移"呢,我們繼續用圖來說明。


結合上面的圖解,我們來說明一下二叉堆的添加元素過程:
1. 將元素2添加在最后一個位置(隊尾)(圖2)。
2. 由於2比其父親6要小,所以將元素2上移,交換2和6的位置(圖3);
3. 然后由於2比5小,繼續將2上移,交換2和5的位置(圖4),此時2大於其父親(根節點)1,結束。
注:這里的節點顏色是為了凸顯,應便於理解,跟紅黑樹的中的顏色無關,不要弄混。。。
看完了這4張圖,是不是覺得二叉堆的添加還是挺容易的,那么下面我們具體看下PriorityQueue的代碼是怎么實現入隊操作的吧。
1 /** 2 * 添加一個元素 3 */ 4 public boolean add(E e) { 5 return offer(e); 6 } 7 8 /** 9 * 入隊 10 */ 11 public boolean offer(E e) { 12 // 如果元素e為空,則排除空指針異常 13 if (e == null) 14 throw new NullPointerException(); 15 // 修改版本+1 16 modCount++; 17 // 記錄當前隊列中元素的個數 18 int i = size ; 19 // 如果當前元素個數大於等於隊列底層數組的長度,則進行擴容 20 if (i >= queue .length) 21 grow(i + 1); 22 // 元素個數+1 23 size = i + 1; 24 // 如果隊列中沒有元素,則將元素e直接添加至根(數組小標0的位置) 25 if (i == 0) 26 queue[0] = e; 27 // 否則調用siftUp方法,將元素添加到尾部,進行上移判斷 28 else 29 siftUp(i, e); 30 return true; 31 }
這里的add方法依然沒有按照Queue的規范,在隊列滿的時候拋出異常,因為PriorityQueue和前面講的ArrayDeque一樣,會進行擴容,所以只有當隊列容量超出int范圍才會拋出異常。
既然PriorityQueue會進行隊列擴容,那么就來看下擴容的具體實現吧(對於數組實現的容器,我們見過太多的擴容了。。。)。
1 /** 2 * 數組擴容 3 */ 4 private void grow(int minCapacity) { 5 // 如果最小需要的容量大小minCapacity小於0,則說明此時已經超出int的范圍,則拋出OutOfMemoryError異常 6 if (minCapacity < 0) // overflow 7 throw new OutOfMemoryError(); 8 // 記錄當前隊列的長度 9 int oldCapacity = queue .length; 10 // Double size if small; else grow by 50% 11 // 如果當前隊列長度小於64則擴容2倍,否則擴容1.5倍 12 int newCapacity = ((oldCapacity < 64)? 13 ((oldCapacity + 1) * 2): 14 ((oldCapacity / 2) * 3)); 15 // 如果擴容后newCapacity超出int的范圍,則將newCapacity賦值為Integer.Max_VALUE 16 if (newCapacity < 0) // overflow 17 newCapacity = Integer. MAX_VALUE; 18 // 如果擴容后,newCapacity小於最小需要的容量大小minCapacity,則按找minCapacity長度進行擴容 19 if (newCapacity < minCapacity) 20 newCapacity = minCapacity; 21 // 數組copy,進行擴容 22 queue = Arrays.copyOf( queue, newCapacity); 23 }
需要理解的是,這里為什么當minCapacity小於0的時候,就代表超出int范圍呢,我們來看下。
int在java中占4個字節,一個字節8位,從0開始記,那么4個字節的最高位就是31,而java中的基本數據類型都是有符號的,所以最高位代表的是符號位。
int的最大值Integer.MAX_VALUE=0111 1111 1111 1111 1111 1111 1111 1111,Integer.MAX_VALUE+1=1000 0000 0000 0000 0000 0000 0000 0000,此時最高位是符號位為1,所以這個數是負數。負數的補碼是在其原碼的基礎上,符號位不變,其余各位取反,最后+1(即在反碼的基礎上+1)。
好了,看完上面這個小插曲,我們來看下二叉堆的一個重要操作"上移"是怎么實現的吧。
1 /** 2 * 上移,x表示新插入元素,k表示新插入元素在數組的位置 3 */ 4 private void siftUp(int k, E x) { 5 // 如果比較器comparator不為空,則調用siftUpUsingComparator方法進行上移操作 6 if (comparator != null) 7 siftUpUsingComparator(k, x); 8 // 如果比較器comparator為空,則調用siftUpComparable方法進行上移操作 9 else 10 siftUpComparable(k, x); 11 } 12 13 private void siftUpComparable(int k, E x) { 14 // 比較器comparator為空,需要插入的元素實現Comparable接口,用於比較大小 15 Comparable<? super E> key = (Comparable<? super E>) x; 16 // k>0表示判斷k不是根的情況下,也就是元素x有父節點 17 while (k > 0) { 18 // 計算元素x的父節點位置[(n-1)/2] 19 int parent = (k - 1) >>> 1; 20 // 取出x的父親e 21 Object e = queue[parent]; 22 // 如果新增的元素k比其父親e大,則不需要"上移",跳出循環結束 23 if (key.compareTo((E) e) >= 0) 24 break; 25 // x比父親小,則需要進行"上移" 26 // 交換元素x和父親e的位置 27 queue[k] = e; 28 // 將新插入元素的位置k指向父親的位置,進行下一層循環 29 k = parent; 30 } 31 // 找到新增元素x的合適位置k之后進行賦值 32 queue[k] = key; 33 } 34 35 // 這個方法和上面的操作一樣,不多說了 36 private void siftUpUsingComparator(int k, E x) { 37 while (k > 0) { 38 int parent = (k - 1) >>> 1; 39 Object e = queue[parent]; 40 if (comparator .compare(x, (E) e) >= 0) 41 break; 42 queue[k] = e; 43 k = parent; 44 } 45 queue[k] = x; 46 }
結合上面的圖解,二叉堆"上移"操作的代碼還是很容易理解的,主要就是不斷的將新增元素和其父親進行大小比較,比父親小則上移,最終找到一個合適的位置。
5.二叉堆的刪除根原理及PriorityQueue的出隊實現
對於二叉堆的出隊操作,出隊永遠是要刪除根元素,也就是最小的元素,要刪除根元素,就要找一個替代者移動到根位置,相對於被刪除的元素來說就是"下移"。



結合上面的圖解,我們來說明一下二叉堆的出隊過程:
1. 將找出隊尾的元素8,並將它在隊尾位置上刪除(圖2);
2. 此時隊尾元素8比根元素1的最小孩子3要大,所以將元素1下移,交換1和3的位置(圖3);
3. 然后此時隊尾元素8比元素1的最小孩子4要大,繼續將1下移,交換1和4的位置(圖4);
4. 然后此時根元素8比元素1的最小孩子9要小,不需要下移,直接將根元素8賦值給此時元素1的位置,1被覆蓋則相當於刪除(圖5),結束。
看完了這6張圖,下面我們具體看下PriorityQueue的代碼是怎么實現出隊操作的吧。
1 /** 2 * 刪除並返回隊頭的元素,如果隊列為空則拋出NoSuchElementException異常(該方法在AbstractQueue中) 3 */ 4 public E remove() { 5 E x = poll(); 6 if (x != null) 7 return x; 8 else 9 throw new NoSuchElementException(); 10 } 11 12 /** 13 * 刪除並返回隊頭的元素,如果隊列為空則返回null 14 */ 15 public E poll() { 16 // 隊列為空,返回null 17 if (size == 0) 18 return null; 19 // 隊列元素個數-1 20 int s = --size ; 21 // 修改版本+1 22 modCount++; 23 // 隊頭的元素 24 E result = (E) queue[0]; 25 // 隊尾的元素 26 E x = (E) queue[s]; 27 // 先將隊尾賦值為null 28 queue[s] = null; 29 // 如果隊列中不止隊尾一個元素,則調用siftDown方法進行"下移"操作 30 if (s != 0) 31 siftDown(0, x); 32 return result; 33 } 34 35 /** 36 * 上移,x表示隊尾的元素,k表示被刪除元素在數組的位置 37 */ 38 private void siftDown(int k, E x) { 39 // 如果比較器comparator不為空,則調用siftDownUsingComparator方法進行下移操作 40 if (comparator != null) 41 siftDownUsingComparator(k, x); 42 // 比較器comparator為空,則調用siftDownComparable方法進行下移操作 43 else 44 siftDownComparable(k, x); 45 } 46 47 private void siftDownComparable(int k, E x) { 48 // 比較器comparator為空,需要插入的元素實現Comparable接口,用於比較大小 49 Comparable<? super E> key = (Comparable<? super E>)x; 50 // 通過size/2找到一個沒有葉子節點的元素 51 int half = size >>> 1; // loop while a non-leaf 52 // 比較位置k和half,如果k小於half,則k位置的元素就不是葉子節點 53 while (k < half) { 54 // 找到根元素的左孩子的位置[2n+1] 55 int child = (k << 1) + 1; // assume left child is least 56 // 左孩子的元素 57 Object c = queue[child]; 58 // 找到根元素的右孩子的位置[2(n+1)] 59 int right = child + 1; 60 // 如果左孩子大於右孩子,則將c復制為右孩子的值,這里也就是找出左右孩子哪個最小 61 if (right < size && 62 ((Comparable<? super E>) c).compareTo((E) queue [right]) > 0) 63 c = queue[child = right]; 64 // 如果隊尾元素比根元素孩子都要小,則不需"下移",結束 65 if (key.compareTo((E) c) <= 0) 66 break; 67 // 隊尾元素比根元素孩子都大,則需要"下移" 68 // 交換跟元素和孩子c的位置 69 queue[k] = c; 70 // 將根元素位置k指向最小孩子的位置,進入下層循環 71 k = child; 72 } 73 // 找到隊尾元素x的合適位置k之后進行賦值 74 queue[k] = key; 75 } 76 77 // 這個方法和上面的操作一樣,不多說了 78 private void siftDownUsingComparator(int k, E x) { 79 int half = size >>> 1; 80 while (k < half) { 81 int child = (k << 1) + 1; 82 Object c = queue[child]; 83 int right = child + 1; 84 if (right < size && 85 comparator.compare((E) c, (E) queue [right]) > 0) 86 c = queue[child = right]; 87 if (comparator .compare(x, (E) c) <= 0) 88 break; 89 queue[k] = c; 90 k = child; 91 } 92 queue[k] = x; 93 }
jdk中,不是直接將根元素刪除,然后再將下面的元素做上移,重新補充根元素;而是找出隊尾的元素,並在隊尾的位置上刪除,然后通過根元素的下移,給隊尾元素找到一個合適的位置,最終覆蓋掉跟元素,從而達到刪除根元素的目的。這樣做在一些情況下,會比直接刪除在上移根元素,或者直接下移根元素再調整隊尾元素的位置少操作一些步奏(比如上面圖解中的例子,不信你可以試一下^_^)。
明白了二叉堆的入隊和出隊操作后,其他的方法就都比較簡單了,下面我們再來看一個二叉堆中比較重要的過程,二叉堆的構造。
6.堆的構造過程
我們在上面提到過的,堆的構造是通過一個heapify方法,下面我們來看下heapify方法的實現。
1 /** 2 * Establishes the heap invariant (described above) in the entire tree, 3 * assuming nothing about the order of the elements prior to the call. 4 */ 5 private void heapify() { 6 for (int i = (size >>> 1) - 1; i >= 0; i--) 7 siftDown(i, (E) queue[i]); 8 }
這個方法很簡單,就這幾行代碼,但是理解起來卻不是那么容器的,我們來分析下。
假設有一個無序的數組,要求我們將這個數組建成一個二叉堆,你會怎么做呢?最簡單的辦法當然是將數組的數據一個個取出來,調用入隊方法。但是這樣做,每次入隊都有可能會伴隨着元素的移動,這么做是十分低效的。那么有沒有更加高效的方法呢,我們來看下。
為了方便,我們將上面我們圖解中的數組去掉幾個元素,只留下7、6、5、12、10、3、1、11、15、4(順序已經隨機打亂)。ok、那么接下來,我們就按照當前的順序建立一個二叉堆,暫時不用管它是否符合標准。
int a = [7, 6, 5, 12, 10, 3, 1, 11, 15, 4 ];

我們觀察下用數組a建成的二叉堆,很明顯,對於葉子節點4、15、11、1、3來說,它們已經是一個合法的堆。所以只要最后一個節點的父節點,也就是最后一個非葉子節點a[4]=10開始調整,然后依次調整a[3]=12,a[2]=5,a[1]=6,a[0]=7,分別對這幾個節點做一次"下移"操作就可以完成了堆的構造。ok,我們還是用圖解來分析下這個過程。



我們參照圖解分別來解釋下這幾個步奏:
1. 對於節點a[4]=10的調整(圖1),只需要交換元素10和其子節點4的位置(圖2)。
2. 對於節點a[3]=12的調整,只需要交換元素12和其最小子節點11的位置(圖3)。
3. 對於節點a[2]=5的調整,只需要交換元素5和其最小子節點1的位置(圖4)。
4. 對於節點a[1]=6的調整,只需要交換元素6和其最小子節點4的位置(圖5)。
5. 對於節點a[0]=7的調整,只需要交換元素7和其最小子節點1的位置,然后交換7和其最小自己點3的位置(圖6)。
至此,調整完畢,建堆完成。
再來回顧一下,PriorityQueue的建堆代碼,看看是否可以看得懂了。
1 private void heapify() { 2 for (int i = (size >>> 1) - 1; i >= 0; i--) 3 siftDown(i, (E) queue[i]); 4 }
int
i = (
size
>>> 1) - 1,這行代碼是為了找尋最后一個非葉子節點,然后倒序進行"下移"siftDown操作,是不是很顯然了。
到這里PriorityQueue的基本操作就分析完了,明白了其底層二叉堆的概念及其入隊、出隊、建堆等操作,其他的一些方法代碼就很簡單了,這里就不一一分析了。
PriorityQueue 完!
參見:
參考資料: