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

  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     }
複製代碼
  這個方法很簡單,就這幾行代碼,但是理解起來卻不是那麼容器的,我們來分析下。
 
     假設有一個無序的數組,要求我們將這個數組建成一個二叉堆,你會怎麼做呢?最簡單的辦法當然是將數組的數據一個個取出來,調用入隊方法。但是這樣做,每次入隊都有可能會伴隨着元素的移動,這麼做是十分低效的。那麼有沒有更加高效的方法呢,我們來看下。
 
     爲了方便,我們將上面我們圖解中的數組去掉幾個元素,只留下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 完!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章