Java中的集合(四)PriorityQueue常用方法
PriorityQueue的基本概念等都在上一篇已說明,感興趣的可以點擊 Java中的集合(三)繼承Collection的Queue接口 查看
這里主要以PriorityQueue的常用方法的學習
一、PriorityQueue的實現
從上圖中給層序遍歷編號,從中可以發現父子節點總有如下的關系:
通過上述三個公式,可以輕易計算出某個節點的父節點以及子節點的下標。這也就是為什么可以直接用數組來存儲堆的原因。
PriorityQueue的peek()和element()操作是常數時間,add()、offer()、 無參數的remove()以及poll()方法的時間復雜度都是log(N)。
二、PriorityQueue常用的方法
三、常用方法剖析
(一)插入元素:add(E e)和offer(E e)
add(E e)和offer(E e)兩者的語義是相同,都是往優先隊列中插入元素,只是Queue接口規定了兩者對插入失敗時采取不同的處理方式。add(E e)方法插入元素失敗時會拋出異常,offer(E e)插入元素失敗時會返回false,對PriorityQueue而言,兩者沒有什么區別。

1 public boolean add(E e) { 2 return offer(e); // add方法內部調用offer方法 3 } 4 public boolean offer(E e) { 5 if (e == null) // 元素為空的話,拋出NullPointerException異常 6 throw new NullPointerException(); 7 modCount++; 8 int i = size; 9 if (i >= queue.length) // 如果當前用堆表示的數組已經滿了,調用grow方法擴容 10 grow(i + 1); // 擴容 11 size = i + 1; // 元素個數+1 12 if (i == 0) // 堆還沒有元素的情況 13 queue[0] = e; // 直接給堆頂賦值元素 14 else // 堆中已有元素的情況 15 siftUp(i, e); // 重新調整堆,從下往上調整,因為新增元素是加到最后一個葉子節點 16 return true; 17 } 18 private void siftUp(int k, E x) { 19 if (comparator != null) // 比較器存在的情況下 20 siftUpUsingComparator(k, x); // 使用比較器調整 21 else // 比較器不存在的情況下 22 siftUpComparable(k, x); // 使用元素自身的比較器調整 23 } 24 private void siftUpUsingComparator(int k, E x) { 25 while (k > 0) { // 一直循環直到父節點還存在 26 int parent = (k - 1) >>> 1; // 找到父節點索引,等同於(k - 1)/ 2 27 Object e = queue[parent]; // 獲得父節點元素 28 // 新元素與父元素進行比較,如果滿足比較器結果,直接跳出,否則進行調整 29 if (comparator.compare(x, (E) e) >= 0) 30 break; 31 queue[k] = e; // 進行調整,新位置的元素變成了父元素 32 k = parent; // 新位置索引變成父元素索引,進行遞歸操作 33 } 34 queue[k] = x; // 新添加的元素添加到堆中 35 }
下面根據圖解演示插入元素過程:
(二)、獲取元素但不刪除隊列首元素:element()和peek()
element()和peek()的語義是相同的,都是獲取元素但不刪除隊列首元素,也就是隊列中權值最下的元素,只是Queue接口規定了兩者刪除元素失敗時的不同處理方式,element()會拋出異常,peek()會返回null。根據小頂堆的特性,堆頂最上層的元素權值是最小的,由於是數組實現的,根據小標關系,小標0既是堆頂的元素,也是數組的第一個元素,所以直接返回下標為0的那個元素即可。

1 // PriorityQueue的peek() 2 public E peek() { 3 if (size == 0) 4 return null; 5 return (E) queue[0];//0下標處的那個元素就是最小的那個 6 } 7 8 // AbstractQueue的element(),由於PriorityQueue繼承自AbstractQueue,所以可以使用element()方法 9 public E element() { 10 E x = peek(); 11 if (x != null) 12 return x; 13 else 14 throw new NoSuchElementException(); 15 }
下面根據圖解演示獲取元素過程:
(三)、獲取並刪除隊列首元素:remove()和poll()
element()和peek()的語義是相同的,都是獲取元素並刪除隊列首元素,只是Queue接口規定了兩者刪除元素失敗時的不同處理方式,remove()會拋出異常,poll()會返回null。由於刪除會影響隊列的結構,所以會通過siftDown()和siftUp()方法調整隊列結構。

1 public E poll() { 2 if (size == 0) 3 return null; 4 int s = --size; 5 modCount++; 6 E result = (E) queue[0];//0下標處的那個元素就是最小的那個 7 E x = (E) queue[s]; 8 queue[s] = null; 9 if (s != 0) 10 siftDown(0, x);//調整 11 return result; 12 } 13 14 public E remove() { 15 E x = poll(); 16 if (x != null) 17 return x; 18 else 19 throw new NoSuchElementException(); 20 } 21 22 private void siftDown(int k, E x) { 23 if (comparator != null) // 比較器存在的情況下 24 siftDownUsingComparator(k, x); // 使用比較器調整 25 else // 比較器不存在的情況下 26 siftDownComparable(k, x); // 使用元素自身的比較器調整 27 } 28 private void siftDownUsingComparator(int k, E x) { 29 int half = size >>> 1; // 只需循環節點個數的一般即可 30 while (k < half) { 31 int child = (k << 1) + 1; // 得到父節點的左子節點索引,即(k * 2)+ 1 32 Object c = queue[child]; // 得到左子元素 33 int right = child + 1; // 得到父節點的右子節點索引 34 if (right < size && 35 comparator.compare((E) c, (E) queue[right]) > 0) // 左子節點跟右子節點比較,取更大的值 36 c = queue[child = right]; 37 if (comparator.compare(x, (E) c) <= 0) // 然后這個更大的值跟最后一個葉子節點比較 38 break; 39 queue[k] = c; // 新位置使用更大的值 40 k = child; // 新位置索引變成子元素索引,進行遞歸操作 41 } 42 queue[k] = x; // 最后一個葉子節點添加到合適的位置 43 }
下面根據圖解演示獲取元素過程:
通過上述代碼和圖解可以看出:
1、首先記錄0
下標處的元素,並用最后一個元素替換0
下標位置的元素,
2、調用siftDown()方法對堆進行調整,最后返回原來0
下標處的那個元素(也就是最小的那個元素)。
重點是siftDown(int k,E e)方法,該方法的作用是從k
指定的位置開始,將x
逐層向下與當前點的左右孩子中較小的那個交換,直到x
小於或等於左右孩子中的任何一個為止。
(四)、刪除隊列中的指定元素:remove(Object o)
remove(Object o)用於刪除隊列中的指定元素(如果隊列中有多個相同元素,只刪除一個),由於刪除操作會改變隊列結構,所以要進行調整;又由於刪除元素的位置可能是任意的,所以調整過程比其它函數稍加繁瑣。

public boolean remove(Object o) { int i = indexOf(o); // 找到數據對應的索引 if (i == -1) // 不存在的話返回false return false; else { // 存在的話調用removeAt方法,返回true removeAt(i); return true; } } private E removeAt(int i) { modCount++; int s = --size; // 元素個數-1 if (s == i) // 如果是刪除最后一個葉子節點 queue[i] = null; // 直接置空,刪除即可,堆還是保持特質,不需要調整 else { // 如果是刪除的不是最后一個葉子節點 E moved = (E) queue[s]; // 獲得最后1個葉子節點元素 queue[s] = null; // 最后1個葉子節點置空 siftDown(i, moved); // 從上往下調整 if (queue[i] == moved) { // 如果從上往下調整完畢之后發現元素位置沒變,從下往上調整 siftUp(i, moved); // 從下往上調整 if (queue[i] != moved) return moved; } } return null; }
具體來說,remove(Object o)可以分為2種情況:
1. 刪除的是最后一個元素。直接刪除即可,不需要調整。
2. 刪除的不是最后一個元素,從刪除點開始以最后一個元素為參照調用siftDown()或siftUp()。
1. 刪除的是最后一個元素。直接刪除即可,不需要調整。
下面根據圖解演示獲取元素過程:
2. 刪除的不是最后一個元素,從刪除點開始以最后一個元素為參照調用siftDown()或siftUp()。
下面根據圖解演示獲取元素過程:
四、PriorityBlockingQueue
(一)、簡介
PriorityBlockingQueue是一個支持優先級的無界阻塞隊列。默認情況下元素采用自然順序升序排列。也可以自定義類實現compareTo()方法來指定元素排序規則,或者初始化PriorityBlockingQueue時,指定構造參數Comparator來對元素進行排序。但需要注意的是不能保證同優先級元素的順序。
(二)、四種構造方法
(三)、定義屬性、數據結構
(四)、以offer(E e)方法說明與PriorityQueue的不同
插入元素源碼:

1 public boolean offer(E e) { 2 if (e == null) 3 throw new NullPointerException(); 4 final ReentrantLock lock = this.lock; 5 lock.lock(); 6 int n, cap; 7 Object[] array; 8 while ((n = size) >= (cap = (array = queue).length)) 9 tryGrow(array, cap); 10 try { 11 Comparator<? super E> cmp = comparator; 12 if (cmp == null) 13 siftUpComparable(n, e, array); 14 else 15 siftUpUsingComparator(n, e, array, cmp); 16 size = n + 1; 17 notEmpty.signal(); 18 } finally { 19 lock.unlock(); 20 } 21 return true; 22 }
和PriorityQueue的實現基本一致區別就是在於加鎖了,並發出了非空信號喚醒阻塞的獲取線程。
通過tryGrow(Object[] array, int oldCap)擴容隊列

1 private void tryGrow(Object[] array, int oldCap) { 2 lock.unlock(); // must release and then re-acquire main lock 3 Object[] newArray = null; 4 if (allocationSpinLock == 0 && 5 UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 6 0, 1)) { 7 try { 8 int newCap = oldCap + ((oldCap < 64) ? 9 (oldCap + 2) : // grow faster if small 10 (oldCap >> 1)); 11 if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow 12 int minCap = oldCap + 1; 13 if (minCap < 0 || minCap > MAX_ARRAY_SIZE) 14 throw new OutOfMemoryError(); 15 newCap = MAX_ARRAY_SIZE; 16 } 17 if (newCap > oldCap && queue == array) 18 newArray = new Object[newCap]; 19 } finally { 20 allocationSpinLock = 0; 21 } 22 } 23 if (newArray == null) // back off if another thread is allocating 24 Thread.yield(); 25 lock.lock(); 26 if (newArray != null && queue == array) { 27 queue = newArray; 28 System.arraycopy(array, 0, newArray, 0, oldCap); 29 } 30 }
從源碼可以看出:為了更好的並發性,其先釋放了全局鎖,然后通過UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,0, 1))設置allocationSpinLockOffset來判斷哪個線程獲得擴容權限,如果沒有獲得權限,就放開CPU資源。后面擴容操作是通過簡單的樂觀鎖allocationSpinLock來進行控制的。
(五)、小結
1、在多線程環境下,可以使用PriorityBlockingQueue 這個優先阻塞隊列。其中add、poll、remove方法都使用 ReentrantLock 鎖來保持同步,take() 方法中如果元素為空,則會一直保持阻塞。
2、由於和PriorityQueue都是繼承自AbstractQueue,所以其它的操作過程都和PriorityQueue的類似,只是定義的方法都使用 ReentrantLock 鎖來保持同步。
五、題外總結
1、jdk內置的優先隊列PriorityQueue內部使用一個堆維護數據,每當有數據add進來或者poll出去的時候會對堆做從下往上的調整和從上往下的調整。
2、PriorityQueue不是一個線程安全的類,如果要在多線程環境下使用,可以使用 PriorityBlockingQueue 這個優先阻塞隊列。其中add、poll、remove方法都使用 ReentrantLock 鎖來保持同步,take() 方法中如果元素為空,則會一直保持阻塞。