給jdk寫註釋系列之jdk1.6容器(12)-PriorityQueue源碼解析

  PriorityQueue是一種什麼樣的容器呢?看過前面的幾個jdk容器分析的話,看到Queue這個單詞你必定會,哦~這是一種隊列。是的,PriorityQueue是一種隊列,可是它又是一種什麼樣的隊列呢?它具備着什麼樣的特色呢?它的底層實現方式又是怎麼樣的呢?咱們一塊兒來看一下。
     PriorityQueue實際上是一個優先隊列,什麼是優先隊列呢?這 和咱們前面講的先進先出(First In First Out )的隊列的區別在於,優先隊列每次出隊的元素都是優先級最高的元素。那麼怎麼肯定哪個元素的優先級最高呢,jdk中使用堆這麼一種數據結構,經過堆使得每次出隊的元素老是隊列裏面最小的,而元素的大小比較方法能夠由用戶指定,這裏就至關於指定優先級嘍。
 
1.二叉堆介紹
 
     那麼堆又是什麼一種數據結構呢、它有什麼樣的特色呢?(如下見於百度百科)
     (1)堆中某個節點的值老是不大於或不小於其父節點的值;
     (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     }
  這個方法很簡單,就這幾行代碼,可是理解起來卻不是那麼容器的,咱們來分析下。
 
     假設有一個無序的數組,要求咱們將這個數組建成一個二叉堆,你會怎麼作呢?最簡單的辦法固然是將數組的數據一個個取出來,調用入隊方法。可是這樣作,每次入隊都有可能會伴隨着元素的移動,這麼作是十分低效的。那麼有沒有更加高效的方法呢,咱們來看下。
 
     爲了方便,咱們將上面咱們圖解中的數組去掉幾個元素,只留下七、六、五、十二、十、三、一、十一、1五、4(順序已經隨機打亂)。ok、那麼接下來,咱們就按照當前的順序創建一個二叉堆,暫時不用管它是否符合標準。
 
     int a = [7, 6, 5, 12, 10, 3, 1, 11, 15, 4 ];
  咱們觀察下用數組a建成的二叉堆,很明顯,對於葉子節點四、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 完!
 
 
參見:
 
 
參考資料:
相關文章
相關標籤/搜索