Java中的集合(四)PriorityQueue常用方法


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 }
View Code

下面根據圖解演示插入元素過程:

(二)、獲取元素但不刪除隊列首元素: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     }
View Code

下面根據圖解演示獲取元素過程:

(三)、獲取並刪除隊列首元素: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 }
View Code

下面根據圖解演示獲取元素過程:


通過上述代碼和圖解可以看出:

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;
}
View Code

具體來說,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 } 
View Code

和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     }
View Code

從源碼可以看出:為了更好的並發性,其先釋放了全局鎖,然后通過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() 方法中如果元素為空,則會一直保持阻塞

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM