深入淺出分析 PriorityQueue
一、摘要
在前幾篇文章中,咱們瞭解到,Queue 的實現類有 ArrayDeque、LinkedList、PriorityQueue。
在上一章節中,陸續的介紹到 ArrayDeque 和 LinkedList 的資料結構和演算法實現,今天咱們來介紹一下** PriorityQueue 這個類,一個特殊的優先順序佇列**。如果有理解不當之處,歡迎指正。
二、簡介
PriorityQueue 並沒有直接實現 Queue介面,而是通過繼承 AbstractQueue 類來實現 Queue 介面一些方法,在 Java 定義中,PriorityQueue 是一個基於優先順序的無界優先佇列。
通俗的說,新增到 PriorityQueue 佇列裡面的元素都經過了排序處理,預設按照自然順序,也可以通過 Comparator 介面進行自定義排序。
優先佇列的作用是保證每次取出的元素都是佇列中權值最小的。
如果猿友們瞭解過 TreeMap 的實現,會發現 PriorityQueue 排序實現與之類似。
PriorityQueue 是採用樹形結構來描述元素的儲存,具體說是通過完全二叉樹實現一個小頂堆,在物理儲存方面,PriorityQueue 底層通過陣列來實現元素的儲存。
在上圖中,我們給每個元素的下標做了標註,足夠細心的你會發現,陣列下標,存在以下關係:
leftNo = parentNo * 2 + 1
rightNo = parentNo * 2 + 2
parentNo = (currentNo -1) / 2
各個引數具體含義如下:
- parentNo:表示父節點下標;
- leftNo:表示子元素左節點下標;
- rightNo:表示子元素右節點下標;
- currentNo:表示當前元素節點下標;
通過上述三個公式,可以輕易計算出某個節點的父節點以及子節點的下標。這也就是為什麼可以直接用陣列來儲存元素實現二叉樹結構的原因。
2.1、原始碼介紹
PriorityQueue 原始碼定義如下:
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable { /**預設容量為11*/ private static final int DEFAULT_INITIAL_CAPACITY = 11; /**佇列容器*/ transient Object[] queue; /**佇列長度*/ private int size = 0; /**比較器,為null使用自然排序*/ private final Comparator<? super E> comparator; ...... }
從定義中可以得出,PriorityQueue 有3個比較核心的變數屬性,內容如下:
- queue:表示存放元素的陣列
- comparator:表示比較器物件,如果為空,使用自然排序
- size:表示佇列長度
我們再來看看 PriorityQueue 類的構造方法,PriorityQueue 構造方法分兩類,一種是預設初始化、另一種是傳入 Comparator 介面比較器,內容如下:
預設初始化,使用自然排序方式進行插入,原始碼如下:
public PriorityQueue() {
//預設陣列長度為11,傳入比較器為null
this(DEFAULT_INITIAL_CAPACITY, null);
}
呼叫的方法,原始碼如下:
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
//初始化容量小於 1,拋異常
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
自定義比較器初始化,使用 comparator 介面比較器作為引數傳入,原始碼如下:
public PriorityQueue(Comparator<? super E> comparator) {
//傳入比較器 comparator
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
這兩者初始化方式,咱們在下文會一一講到。
在介紹 PriorityQueue 實現的方法之前,咱們瞭解到,Queue 介面定義有如下方法:
同樣的 PriorityQueue 也實現了這些方法,PriorityQueue 方法雖然定義的很多,但無非就是對容器進行新增、刪除、查詢操作,下面我們分別來看看各個操作方法的實現過程。
三、常見方法介紹
3.1、新增方法
PriorityQueue 的新增方法有 2 種,分別是add(E e)
和offer(E e)
,兩者語義相同,都是向優先佇列中插入元素,只是Queue
介面規定二者對插入失敗時的處理不同,前者在插入失敗時丟擲異常,後則返回false
。
3.1.1、offer 方法
offer 方法圖解實現流程如下:
新加入的元素可能會破壞小頂堆的性質,在 c、d 兩步會進行調整。
offer 方法的實現,原始碼如下:
public boolean offer(E e) {
//不允許放入null元素
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;
}
值得注意的是,插入元素不能為null
,否則報空指標異常!
當陣列空間不足時,會進行擴容,擴容函式grow()
類似於ArrayList
裡的grow()
函式,就是再申請一個更大的陣列,並將原陣列的元素複製過去,原始碼如下:
private void grow(int minCapacity) {
int oldCapacity = queue.length;
//如果舊陣列容量小於64,新容量為 oldCapacity *2 +2
//如果大於64,新容量為 oldCapacity + oldCapacity * 0.5
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
//判斷是否超過最大容量值,設定最高容量值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//複製陣列元素
queue = Arrays.copyOf(queue, newCapacity);
}
從原始碼中可以看出,在計算新容量的時候,如果舊陣列的容量小於64
,新陣列容量為舊容量的2
倍➕2
;反之,新陣列容量的擴容係數為50%
。
我們再來看看siftUp(i, e)
這個方法,當插入的元素不是頂部位置,會進行內容排序調整,siftUp(i, e)
方法就是起到這個作用,原始碼如下:
private void siftUp(int k, E x) {
//如果使用比較器,採用比較器進行比較
if (comparator != null)
siftUpUsingComparator(k, x);
else
//沒有比較器,採用自然排序
siftUpComparable(k, x);
}
預設調整方式的實現,原始碼如下:
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
//parentNo = (nodeNo-1)/2
int parent = (k - 1) >>> 1;
Object e = queue[parent];
//預設自然排序,從小到大
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
自定義比較器的實現,調整方式,原始碼如下:
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
//parentNo = (nodeNo-1)/2
int parent = (k - 1) >>> 1;
Object e = queue[parent];
//呼叫比較器的比較方法
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
預設的插入規則中,新加入的元素可能會破壞小頂堆的性質,因此需要進行調整。
調整的過程為:從尾部下標的位置開始,將加入的元素逐層與當前點的父節點的內容進行比較並交換,直到滿足父節點內容都小於子節點的內容
為止。
當然,也可以依靠自定義比較器,實現自定排序規則。
3.1.2、add 方法
add
方法,就比較簡單了,直接呼叫了offer
方法,返回false
拋異常,原始碼如下:
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
3.1.3 使用方式
- 自然排序
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
System.out.println("插入的資料");
//隨機新增兩位數
for (int i = 0; i < 10; i++) {
Integer num = new Random().nextInt(90) + 10;
System.out.print(num + ",");
queue.offer(num);
}
System.out.println("\n輸出後的資料");
while (true){
Integer result = queue.poll();
if(result == null){
break;
}
System.out.print(result + ",");
}
}
輸出結果:
插入的資料
53,97,66,58,69,10,72,27,18,16,
輸出後的資料
10,16,18,27,53,58,66,69,72,97,
- 自定義排序
public static void main(String[] args) {
PriorityQueue<Integer> customeQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//按照大到小排序
return o2.compareTo(o1);
}
});
System.out.println("插入的資料");
//隨機新增兩位數
for (int i = 0; i < 10; i++) {
Integer num = new Random().nextInt(90) + 10;
System.out.print(num + ",");
customeQueue.offer(num);
}
System.out.println("\n輸出後的資料");
while (true){
Integer result = customeQueue.poll();
if(result == null){
break;
}
System.out.print(result + ",");
}
}
輸出結果:
插入的資料
66,39,28,54,56,66,54,77,10,97,
輸出後的資料
97,77,66,66,56,54,54,39,28,10,
3.2、刪除方法
PriorityQueue 的刪除方法有 2 種,分別是remove()
和poll()
,兩者語義也完全相同,都是獲取並刪除隊首元素,區別是當方法失敗時前者丟擲異常,後者返回null
。由於刪除操作會改變佇列的結構,為維護小頂堆的性質,需要進行必要的調整。
3.2.1、poll 方法
offer 方法圖解實現流程如下:
刪除的元素可能會破壞小頂堆的性質,在 b、 c、d 三步會進行調整。
poll 方法的實現,原始碼如下:
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
//0下標處的那個元素就是最小的那個
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
//調整
siftDown(0, x);
return result;
}
調整過程與插入的調整過程有些相反!
首先記錄陣列頭部的下標,並用最後一個元素的內容替換陣列頭部的元素,之後呼叫siftDown()
方法對堆進行調整,最後返回陣列頭部的元素。
siftDown(int k, E x)
方法的實現,原始碼內容如下:
private void siftDown(int k, E x) {
//判斷是否有自定義比較器
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
與插入的調整類似,首先判斷是否有自定義的比較器,如果沒有,按照預設的方式進行調整,反之,根據自定義比較器的排序規則進行調整。
預設調整方式,函式siftDownComparable(k, x)
,原始碼如下:
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
//首先找到左右孩子中較小的那個,記錄到c裡,並用child記錄其下標
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;//然後用c取代原來的值
k = child;
}
queue[k] = key;
}
自定義調整方式,函式siftDownUsingComparator(k, x)
,原始碼如下:
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
//首先找到左右孩子中較小的那個,記錄到c裡,並用child記錄其下標
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;//然後用c取代原來的值
k = child;
}
queue[k] = x;
}
預設的刪除調整中,首先獲取頂部下標和最尾部的元素內容,從頂部的位置開始,將尾部元素的內容逐層向下與當前點的左右子節點中較小的那個交換,直到判斷元素內容小於或等於左右子節點中的任何一個為止。
如果有自定義比較器,使用自定義比較器中的排序演算法來進行交換。
思路是一樣的,只是排序比較演算法不一樣而已!
3.2.2、remove 方法
remove 方法實現比較簡單,直接呼叫了poll()
方法,返回空值拋異常,原始碼如下:
public E remove() {
E x = poll();
if (x != null)
return x;
else
//返回空值,拋異常
throw new NoSuchElementException();
}
3.3、查詢方法
PriorityQueue 的查詢方法有 2 種,分別是element()
和和peek()
,兩者語義也完全相同,都是獲取但不刪除隊首元素,也就是佇列中權值最小的那個元素,二者唯一的區別是當方法失敗時前者丟擲異常,後者返回null
。
因為是陣列結構,所以查詢的時間複雜度log(1)
,根據小頂堆的性質,堆頂那個元素就是全域性最小的那個,直接返回陣列下標為0
即可返回隊首元素!
3.3.1、peek 方法
peek 方法圖解實現流程如下:
peek 方法實現,直接返回陣列下標為0
的元素,原始碼如下:
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
3.3.2、element 方法
element 方法實現也比較簡單,直接呼叫了peek()
方法,如果返回空值拋異常,原始碼如下:
public E element() {
E x = peek();
if (x != null)
return x;
else
//返回空值,拋異常
throw new NoSuchElementException();
}
四、總結
在 Java 中 PriorityQueue 是一個使用陣列結構來儲存元素的優先佇列,雖然它也實現了Queue
介面,但是元素存取並不是先進先出,而是通過一個二叉小頂堆實現的,預設底層使用自然排序規則給插入的元素進行排序,也可以使用自定義比較器來實現排序,每次取出的元素都是佇列中權值最小的。
同時需要注意的是,PriorityQueue 不能插入null
,否則報空指標異常!
五、參考
1、JDK1.7&JDK1.8 原始碼
2、知乎 - CarpenterLee -深入理解Java PriorityQueue
作者:炸雞可樂
原文出處:www.pzblog.cn