jdk原始碼分析之PriorityQueue
基本原理
PriorityQueue(優先順序佇列)的資料結構是最小堆,採用陣列作為底層資料結構。
不同於普通的遵循FIFO規則的佇列,PriorityQueue每次都選出優先順序最高的元素出隊,優先佇列裡實際是維護最小堆,通過最小堆使得每次取出的元素總是優先順序最高的。
/**
* Priority queue represented as a balanced binary heap: the two
* children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
* priority queue is ordered by comparator, or by the elements'
* natural ordering, if comparator is null: For each node n in the
* heap and each descendant d of n, n <= d. The element with the
* lowest value is in queue[0], assuming the queue is nonempty.
*/
transient Object[] queue; // non-private to simplify nested class access
底層採用Object陣列作為最小堆的實現方式
節點queue[n]的左孩子節點為queue[2*n+1] ,右孩子節點為queue[2*(n+1)]。
queue[0]表示優先順序最高的節點
新增資料offer
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
首先入口引數檢查,PriorityQueue不支援儲存null,所以如果給PriorityQueue新增null,丟擲空指標異常,在錯誤發生後儘快檢測出錯誤,符合Effective Java第38條原則《檢查引數的有效性》。
接著修改modCount,方便在迭代的過程中發生結構性修改可以丟擲ConcurrentModificationException異常
因為是新增資料,所以需要判斷是不是需要擴容
if (i >= queue.length)
grow(i + 1);
如果原佇列為空,就把新新增的資料新增到隊首
if (i == 0)
queue[0] = e;
否則就呼叫siftUp進行向上篩選
else
siftUp(i, e);
篩選分兩種情況,因為有兩種排序規則,按照資料自身的排序規則呼叫siftUpComparable或者按照外部規定的排序規則呼叫siftUpUsingComparator
siftUp(int k, E x)的含義為:在堆的陣列下標k處,放置了一個資料x,此操作有可能破壞堆的性質,因此對堆進行調整
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
siftUpComparable和siftUpUsingComparator程式碼邏輯類似,只是比較規則不同,因此只取其一進行分析
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
首先將需要插入的資料x強制轉換為可比較的物件Comparable並儲存為key
然後開始向上篩選,篩選終止的條件有兩個,第一個是待插入的資料比堆頂元素都小,因此會篩選到下標k==0,此時結束迴圈;第二個是在向上篩選的過程中找到一個父節點比待插入節點小,此時不需要繼續向上篩選了,break退出迭代。
迭代的過程中,首先獲取父節點在陣列中的下標位置並將父節點的資料儲存為e
int parent = (k - 1) >>> 1;
Object e = queue[parent];
然後判斷如果待插入節點比父節點大,break退出迴圈,待插入節點找到了自己的位置
if (key.compareTo((E) e) >= 0)
break;
否則的話,將父節點的資料(陣列下標(k - 1) >>> 1)儲存到正在篩選的節點(陣列下標k),正在篩選的節點的下標k設為父節點下標(k - 1) >>> 1,進行下一次迭代
queue[k] = e;
k = parent;
迭代結束後,將待插入資料儲存到在堆中合適的位置
queue[k] = key;
獲取並刪除隊首poll
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
首先檢查佇列容量,size==0表示佇列此時沒有資料,從沒有資料的佇列中讀取資料直接返回null
然後對size和modCount進行修改,儲存陣列最後一個元素下標為s=size-1
int s = --size;
modCount++;
最小堆的性質保證優先順序佇列的隊首元素queue[0]一定是最小的(優先順序最高的),因此將隊首元素儲存為result
E result = (E) queue[0];
隊首元素出佇列後,將隊尾元素儲存到隊首,隊尾元素置null加速垃圾回收,這個操作有可能破壞了堆的性質,因此需要向下篩選
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
siftDown(int k, E x)的含義為:將優先順序佇列陣列下標為k處的資料設定為x,此操作有可能破壞了堆的性質,因此需要調整。
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
和向上篩選一樣,分兩種排序規則進行分類處理,但是兩種情況程式碼大同小異,只是比較規則不一樣,選其一進行分析
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
首先找到葉子節點的最小下標,也就是第一個葉子節點的下標,並儲存為half。
int half = size >>> 1;
最小堆也是一個完全二叉樹,完全二叉樹的性質決定了size的一半減一(size/2-1)是非葉子節點的最大下標,size的一半(size/2)是葉子節點的最小下標
然後向下篩選,要麼找到了比篩選到了葉子節點,要麼找到了資料x在堆中的合適位置,這兩種情況都停止篩選
篩選的過程中,需要將待篩選節點和兩個孩子節點進行比較,如果待篩選節點比兩個孩子都小,則向下篩選結束,否則交換較小孩子和待篩選節點的位置,進行下一輪篩選。
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
然後左右孩子比較,設定左右孩子的較小者為c,然後用c和待篩選節點x進行比較
if (comparator.compare(x, (E) c) <= 0)
break;
如果待篩選節點x比左右孩子的較小者c都小,說明待篩選節點x找到了自己的合適位置,停止篩選,break退出迴圈
否則進行下一輪篩選
queue[k] = c;
k = child;
將左右孩子較小者c儲存到父節點queue[k] ,設定下一輪待篩選節點下標k為左右孩子較小者下標,進行新的一輪篩選
篩選過程結束,將資料x放置到自己在最小堆中的最終位置
queue[k] = x;
建堆過程
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
建堆的過程只從非葉子節點開始篩選,因為篩選的過程中會處理到葉子節點。並且篩選的過程是倒著來的,從最後一個非葉子節點開始,一直處理到陣列中第一個節點,這種自底向上的思想類似於動態規劃。
相關文章
- 死磕 java集合之PriorityQueue原始碼分析Java原始碼
- jdk原始碼分析之TreeMapJDK原始碼
- 原始碼|jdk原始碼之HashMap分析(一)原始碼JDKHashMap
- 原始碼|jdk原始碼之HashMap分析(二)原始碼JDKHashMap
- PriorityQueue原理分析——基於原始碼原始碼
- 死磕 jdk原始碼之HashMap原始碼分析JDK原始碼HashMap
- JDK1.8原始碼分析之HashMapJDK原始碼HashMap
- 【JDK】JDK原始碼分析-ReentrantLockJDK原始碼ReentrantLock
- 原始碼|jdk原始碼之棧、佇列及ArrayDeque分析原始碼JDK佇列
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(3)JDK原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(2)JDK原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(1)JDK原始碼
- JDK原始碼分析-TreeSetJDK原始碼
- JDK中的BitMap實現之BitSet原始碼分析JDK原始碼
- JDK 1.6 HashMap 原始碼分析JDKHashMap原始碼
- JDK原始碼分析(四)——LinkedHashMapJDK原始碼HashMap
- JDK1.8原始碼分析03之idea搭建原始碼閱讀環境JDK原始碼Idea
- 原始碼分析–ArrayList(JDK1.8)原始碼JDK
- 原始碼分析–HashSet(JDK1.8)原始碼JDK
- ArrayList原始碼分析 jdk1.8原始碼JDK
- JDK 原始碼分析(1) Object類JDK原始碼Object
- HashMap原始碼分析(JDK8)HashMap原始碼JDK
- LinkedList原始碼分析(jdk1.8)原始碼JDK
- JDK1.8 hashMap原始碼分析JDKHashMap原始碼
- HashMap原始碼分析 JDK1.8HashMap原始碼JDK
- ConcurrentHashMap原始碼分析-JDK18HashMap原始碼JDK
- ArrayList原始碼分析(JDK1.8)原始碼JDK
- 原始碼分析系列1:HashMap原始碼分析(基於JDK1.8)原始碼HashMapJDK
- 併發程式設計之 ConcurrentHashMap(JDK 1.8) putVal 原始碼分析程式設計HashMapJDK原始碼
- JDK原始碼解析系列之objectJDK原始碼Object
- 深入分析Java中的PriorityQueue底層實現與原始碼Java原始碼
- JDK1.8 原始碼分析(九)--LinkedHashMapJDK原始碼HashMap
- JDK1.8 原始碼分析(十) -- TreeMapJDK原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- JDK1.8原始碼分析01之學習建議(可以延伸其他原始碼學習)JDK原始碼
- JDK1.8原始碼分析筆記-HashMapJDK原始碼筆記HashMap
- 原始碼分析–ConcurrentHashMap與HashTable(JDK1.8)原始碼HashMapJDK