PriorityQueue
容器,翻一下就是優先隊列
,平常一般稱堆
。優先隊列
的作用是將隊列中的元素最小值
放到堆頂
(小頂堆
,最小的元素在頂端
,你也可以修改comparator
,使之變成大頂堆
,最大的元素在頂端
)。每當我們插入
、刪除
元素,都是從堆頂
進行,優先隊列
會自動重新調整堆
,將最小的元素值調整到堆頂
。
本篇博客將從源碼的角度並結合相應的圖解,對Java
中的PriorityQueue
容器的實現原理進行分析。爲了幫助大家更好的理解PriorityQueue
,寫了一篇關於堆的實現與原理分析博客,牆裂推薦各位小夥伴閱讀做鋪墊,鏈接→數據結構之堆(我猜,關於堆的這些維護細節,你肯定不清楚,不信你來看!)
註明:以下源碼分析都是基於jdk 1.8.0_221
版本
Java容器之PriorityQueue源碼分析目錄
一、PriorityQueue
容器概述
PriorityQueue
類的申明如下:
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable
Java
中的PriorityQueue
容器,底層是通過數組
來實現堆結構
。在前一篇博客數據結構之堆(我猜,關於堆的這些維護細節,你肯定不清楚,不信你來看!)說過,基於數組實現的堆兩個重要規律:
- 下標爲
index
的左、右孩子的下標分別是index * 2 + 1
、(index + 1) * 2
- 堆中有子節點的節點最大下標爲
size / 2 - 1
(注意:size
爲堆的大小,不是數組的大小)
二、PriorityQueue
類中的主要屬性
/**
* 數組默認初始化的長度
*/
private static final int DEFAULT_INITIAL_CAPACITY = 11;
/**
* 實現堆的數組
*/
transient Object[] queue; // non-private to simplify nested class access
/**
* 優先隊列中的元素數(注意需要與數組的長度區分開)
*/
private int size = 0;
/**
* 優先隊列中元素比較器,通過指定comparator,可以選擇構造小頂堆還是大頂堆
*/
private final Comparator<? super E> comparator;
/**
* 結構性調整(插入、刪除、擴容等操作)的次數
*/
transient int modCount = 0;
三、PriorityQueue
類的構造器
/**
* 默認構造器,數組長度初始化爲默認值
*/
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
/**
* 指定數組的初始化長度
*/
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
/**
* 指定元素比較器
*/
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
/**
* 數組長度、元素比較器都指定
*/
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
// 數組長度不能小於1
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
/**
* 複製構造器1
* 將容器1中的元素全部放入當前創建的優先隊列中
*/
@SuppressWarnings("unchecked")
public PriorityQueue(Collection<? extends E> c) {
if (c instanceof SortedSet<?>) {
// 如果容器C是SortedSet接口實現類,可初始化comparator
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
// 在將容器中的元素全部插入堆中
initElementsFromCollection(ss);
}
else if (c instanceof PriorityQueue<?>) {
// 如果容器c是PriorityQueue的子類,直接賦值comparator、pq即可
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
}
else {
// 否則comparator無法初始化,只能期望c中的元素實現了comparable接口
this.comparator = null;
initFromCollection(c);
}
}
/**
* 複製構造器2
*/
@SuppressWarnings("unchecked")
public PriorityQueue(PriorityQueue<? extends E> c) {
// 如果容器c是PriorityQueue的子類,直接賦值comparator、pq即可
this.comparator = (Comparator<? super E>) c.comparator();
initFromPriorityQueue(c);
}
/**
* 複製構造器3
*/
@SuppressWarnings("unchecked")
public PriorityQueue(SortedSet<? extends E> c) {
// 如果容器C是SortedSet接口實現類,可初始化comparator
this.comparator = (Comparator<? super E>) c.comparator();
initElementsFromCollection(c);
}
四、PriorityQueue
容器中的堆維護
方法
1、擴容
/**
* 基於數組實現的堆,只要複製到一個更長的數組即可
*/
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 如果oldCapacity < 64,擴大爲原來的2倍,否則擴大原來的一半
int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));
// 如果newCapacity超過了最大值MAX_ARRAY_SIZE,則調用hugeCapacity方法
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 然後將原數組擴複製更長的數組
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
// 當minCapacity >= 2^31時,由於最高位是符號位,並且是補碼存儲,所以此時minCapacity < 0
if (minCapacity < 0)
throw new OutOfMemoryError();
// 返回max(Integer.MAX_VALUE, MAX_ARRAY_SIZE)
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
2、插入
元素(上浮
)
在優先隊列
中插入
元素,先放入數組中的下一個空閒位置,然後根據下標的邏輯關係,將插入
的元素上浮
。
/**
* 往優先隊列中插入一個元素
*/
public boolean add(E e) {
return offer(e);
}
/**
* 往優先隊列中插入一個元素
*/
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;
// 如果i == 0,說明插入前,優先隊列中沒有元素,此時直接放入堆頂,無需進行調整
if (i == 0)
queue[0] = e;
else
// 否則調用siftUp方法,將e放入queue[i],然後上浮插入元素
siftUp(i, e);
return true;
}
/**
* 將x放入queue[k],然後上浮插入元素
*/
private void siftUp(int k, E x) {
// 如果優先隊列容器指定了comparator,則使用comparator進行上浮
// 否則只能寄託於元素E的類型實現了Comparable接口
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
/**
* 使用Comparable中的compare方法進行上浮插入節點
*/
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
// 傳入時的k是堆的尾端
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],並且將插入節點index轉移到父節點的下標,繼續上浮
queue[k] = e;
k = parent;
}
// 退出while循環時,k是真正應該插入的位置,將key賦值到該下標對應的位置即可
queue[k] = key;
}
/**
* 使用comparator比較器進行上浮插入節點,與上面的方法是一樣的
*/
@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
// 傳入時的k是堆的尾端
while (k > 0) {
// 計算當前插入位置的邏輯父節點下標
int parent = (k - 1) >>> 1;
Object e = queue[parent];
// 如果父節點比插入節點小,停止上浮
if (comparator.compare(x, (E) e) >= 0)
break;
// 否則將父節點放入queue[k],並且將插入節點index轉移到父節點的下標,繼續上浮
queue[k] = e;
k = parent;
}
// 退出while循環時,k是真正應該插入的位置,將key賦值到該下標對應的位置即可
queue[k] = x;
}
3、刪除
元素(下沉
)
Java
中的PriorityQueue
不但實現了刪除頂端
元素的接口,而且還實現了刪除任意
位置元素的接口。不過兩者的操作是相似的,先用堆尾元素替換刪除元素,然後對替換後的元素進行下沉
操作。
/**
* 移除堆頂元素
*/
@SuppressWarnings("unchecked")
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)
// 將x放入queue[0],並下沉元素
siftDown(0, x);
return result;
}
/**
* 將元素x放入queue[x]然後下沉queue[x]
*/
private void siftDown(int k, E x) {
// 如果優先隊列容器指定了comparator,則使用comparator進行上浮
// 否則只能寄託於元素E的類型實現了Comparable接口
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
/**
* 使用Comparable中的compare方法進行下沉元素
*/
@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
// size >>> 1 - 1是最大的有子節點的節點下標
int half = size >>> 1;
// 當k < half,此時說明queue[k]在邏輯上存在子節點,可能需要下沉
while (k < half) {
// (k << 1) + 1爲k的左子節點下標
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
// 如果存在右子節點,並且左子節點 > 右子節點,則更新下標child爲右子節點的下標
if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
// 現在c指向左右子節點較小值,child指向較小者的下標
// 如果key <= 左右子節點較小者,停止下沉
if (key.compareTo((E) c) <= 0)
break;
// 將左右子節點較小值移動到queue[k],然後更新k爲較小者的下標,繼續下沉
queue[k] = c;
k = child;
}
// 退出while循環後,說明key需要放入queue[k],不需要再下沉
queue[k] = key;
}
/**
* 使用comparator比較器進行下沉元素,與上面的方法是一樣的
*/
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
// size >>> 1 - 1是最大的有子節點的節點下標
int half = size >>> 1;
// 當k < half,此時說明queue[k]在邏輯上存在子節點,可能需要下沉
while (k < half) {
// (k << 1) + 1爲k的左子節點下標
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
// 如果存在右子節點,並且左子節點 > 右子節點,則更新下標child爲右子節點的下標
if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
// 現在c指向左右子節點較小值,child指向較小者的下標
// 如果key <= 左右子節點較小者,停止下沉
if (comparator.compare(x, (E) c) <= 0)
break;
// 將左右子節點較小值移動到queue[k],然後更新k爲較小者的下標,繼續下沉
queue[k] = c;
k = child;
}
// 退出while循環後,說明key需要放入queue[k],不需要再下沉
queue[k] = x;
}
4、初始化
前面在介紹PriorityQueue
的構造器的時候,有幾個複製構造器,其中一個是從Collection實現類中提取元素放入堆中。PriorityQueue
類並沒有一個一個的放入堆中,再調整,而是一次性讀取所有元素,然後統一調整。
/**
* 從Collection接口實現類中進行初始化
*/
private void initFromCollection(Collection<? extends E> c) {
// 將容器c中的元素提取到queue數組
initElementsFromCollection(c);
// 進行統一的堆調整
heapify();
}
/**
* 將容器c中的元素提取到queue數組
*/
private void initElementsFromCollection(Collection<? extends E> c) {
Object[] a = c.toArray();
// 可能c.toArray返回的不是Object[]數組,在強轉複製一遍
if (a.getClass() != Object[].class)
a = Arrays.copyOf(a, a.length, Object[].class);
// 檢查數組中是否存在null對象
int len = a.length;
if (len == 1 || this.comparator != null)
for (int i = 0; i < len; i++)
if (a[i] == null)
throw new NullPointerException();
// 修改this對象的queue引用,size大小(這裏並沒有調整堆)
this.queue = a;
this.size = a.length;
}
/**
* 調整整個queue數組
*/
@SuppressWarnings("unchecked")
private void heapify() {
// (size >>> 1) - 1是最大的有子節點的節點下標
for (int i = (size >>> 1) - 1; i >= 0; i--)
// 從底往上,對每一個有子節點的元素進行下沉操作(前面刪除元素介紹過這個方法,不要跳着看哦。。。)
siftDown(i, (E) queue[i]);
}
五、總結
經過上面的源碼分析,可以看出PriorityQueue
容器的本質就是封裝了queue數組
,每次插入元素放到堆尾
,然後上浮
插入元素;每次刪除堆頂
元素時,將堆尾
元素移動到堆頂
,然後下沉
該元素。