PriorityQueue
一個無限的優先級隊列基于一個優先級堆。優先級隊列中的元素根據它們的Comparable自然順序或通過在隊列構造時提供的Comparator來排序。(如果有Comparator就根據Comparator來對元素進行排序,否則根據元素自己的Comparable來進行排序)。一個優先級隊列不允許‘null’元素。一個依賴自然排序的優先級隊列甚至不允許插入一個不可比較(non-comparable)的對象(如果你插入一個non-comparable對象,則會拋出一個ClassCastException異常)。
隊列的頭(head)元素是相對于指定順序的最小的(least)元素。如果多個元素被綁為最小值,那么頭元素是它們中的一個————綁定會被任意的破壞。隊列的檢索操作poll、remove、peek和element都會訪問隊列頭(head)元素。
一個優先級隊列是無限制的,但是它有一個內部的“capacity”管理著數組的大小,該數組用于存儲隊列的元素。它總是至少同隊列大小一樣大。當元素加到優先級隊列中,它的容量會自動增加。并沒有指定增長策略的細節。
該類和它的迭代器實現了Collection和Iterator接口所有可選的方法。迭代器提供的iterator()方法不保證遍歷優先級隊列的元素根據任何特別的順序。如果你需要有序的遍歷,考慮使用Arrays.sort(pq.toArray()).
注意,PriorityQueue類的實現是非同步的。如果任何一個線程修改隊列,多線程不應該同時訪問一個PriorityQueue實例。相反,應該使用線程安全的PriorityBlockingQueue類。
實現注意:該實現提供了O(log(n))時間復雜度對于入隊和出隊方法:offer、poll、remove()和add;線性的時間O(n)對于remove(object)和contains(object)方法;和常量的時間O(1)對于檢索方法:peek、element和size。
屬性
/**
* 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
優先級隊列表現為一個平衡二項堆(即,平衡二叉樹):queue[n]的兩個兒子分別是queue[2n+1]和queue[2(n+1)]。優先級隊列通過比較器(comparator)來排序,或者如果比較器為空則通過元素的自然順序來排序:堆中每個節點n和n的每個后裔節點d,n <= d。假設隊列是非空的,那么具有最低值的元素在queue[0]。
優先級隊列的數據結構是一個平衡二叉樹,并且數中所有的子節點必須大于等于父節點,而同一層子節點間無需維護大小關系。這樣的結構性讓優先級隊列看起來像是一個最小堆。
① 假設父節點為queue[n],那么左孩子節點為queue[2n+1],右孩子節點為queue[2(n+1)]。
② 假設孩子節點(無論是左孩子節點還是右孩子節點)為queue[n],n>0。那么父節點為queue[(n-1) >>> 1]
節點間的大小關系:
① 父節點總是小于等于孩子節點
② 同一層孩子節點間的大小無需維護
葉子節點與非葉子節點:
① 一個長度為size的優先級隊列,當index >= size >>> 1時,該節點為葉子節點。否則,為非葉子節點。"附"中會對該結論做個簡單的證明。
/**
* 優先級隊列元素的個數
*/
private int size = 0;
/**
* 優先級隊列結構上被修改的次數。修改操作包括:clear()、offer(E)、poll()、removeAt(int)
*/
transient int modCount = 0; // non-private to simplify nested class access
方法
- 添加節點
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;
}
往優先級隊列中插入元素,如果隊列滿了,則進行擴容。插入操作必要的話是會導致堆元素調整的,以滿足父節點總是小于等于子節點的要求。
插入操作的時間復雜度為O(log(n));
通過siftUp方法來完成元素插入時的調整:siftUp(index, object)方法會升高待插入元素在樹中的位置index,直到待插入的元素大于或等于它待插入位置的父節點
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
@SuppressWarnings("unchecked")
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;
}
@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
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;
}
通過“int parent = (k - 1) >>> 1;”獲取到當前要插入節點位置的父節點,比較父節點和待插入節點,如果待插入節點小于父節點,則將父節點插入到子節點的位置,然后在獲取父節點的父節點循環上面的操作,直到待插入節點大于等于父節點,則在相應位置插入這個節點。最終保證代表優先級隊列的平衡二叉樹中,所有的子節點都大于它們的父節點,但同一層的子節點間并不需要維護大小關系。
圖解“添加節點”步驟:
往一個空的優先級隊列中依次插入“13”、“-3”、“20”、“-25”
① 插入“13”
② 插入“-3”
③ 插入“20”
④ 插入“-25”
- 獲取優先級隊列頭結點
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
獲取優先級隊列頭元素,也就是優先級隊列中值最小的元素。
獲取操作的時間復雜度為O(1)
- 刪除節點
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
該刪除操作的最壞耗時為:n + 2log(n); 所以該操作的的時間復雜度為O(n);
indexOf(object)操作時間復雜度為O(n);
removeAt(index)操作時間復雜度為O(log(n))
private E removeAt(int i) {
// assert i >= 0 && i < size;
modCount++;
int s = --size;
if (s == i) // removed last element
queue[i] = null;
else {
E moved = (E) queue[s];
queue[s] = null;
siftDown(i, moved);
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
如果待刪除節點位置為隊列尾,則直接將隊列尾位置置null。否則將隊列尾節點前插以覆蓋待刪除節點位置的節點。
當待刪除節點的位置為非葉子節點時,會進行一系列的節點調整,使得隊尾節點在前插后能保證優先級隊列數據結構的正確性。
當待刪除節點的位置為葉子節點時,會先將隊尾節點設置到待刪除節點位置以使得隊列中已經沒有待刪除節點了,然后再進行已經插入到新位置的隊尾節點同它新父節點進行比較調整,以保證父節點總是小于等于子節點,即保證優先級隊列數據結構的正確性。
當該方法進行siftUp操作來對節點進行結構調整后使得隊尾節點最終并不是被設置到了待刪除節點位置,這時就返回這個前插的隊尾元素。因為這種情況下,刪除操作會涉及到一個未訪問的元素被移動到了一個已經訪問過的節點位置。在迭代器操作中需要特殊處理。
通過siftDown方法來完成元素移除時的調整:siftDown(index, object)方法會降低待插入元素在樹中的位置index,直到待插入的元素小于或等于它待插入位置的孩子節點。
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
@SuppressWarnings("unchecked")
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) {
int child = (k << 1) + 1; // assume left child is least
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;
k = child;
}
queue[k] = key;
}
@SuppressWarnings("unchecked")
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;
}
因為在平衡二叉樹中,葉子節點的個數總是大于等于前面所有非葉子節點個數之和。所有如果待刪除元素的所在位置大于等于隊列長度的一半,則說明待刪除的節點是一個葉子節點,則直接將隊列中最后一個節點值(注意,隊列中最后一個節點一定也是葉子節點)設置到待刪除節點所在位置。
如果待刪除節點的位置小于隊列長度的一半,則說明待刪除的節點是一個非葉子節點。那么先取得待刪除節點的子節點中小的那個子節點,將該子節點與隊列中最后一個節點進行比較,如果子節點小于隊列中最后一個節點,則將子節點值設置到待刪除節點的位置,然后再次獲取當前子節點的較小的子節點重復一樣的操作,直到隊列最后一個節點比較小的那個子節點還要小,則將隊列最后一個節點值設置為這個子節點的父節點。最終保證代表優先級隊列的平衡二叉樹中,所有的父節點都小于等于它的子節點,但同一層的子節點間并不需要維護大小關系。
圖解“刪除節點”步驟:
假設有如下優先級隊列:
情況二:刪除“queue[2]=-23”
- 是否包含節點
public boolean contains(Object o) {
return indexOf(o) != -1;
}
判斷優先級隊列中是否包含object對象。該方法的時間復雜度為:O(n)
private int indexOf(Object o) {
if (o != null) {
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
return -1;
}
- 移除并獲取優先級隊列頭節點
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;
}
移除并獲取優先級隊列頭節點。該操作的時間復雜度為:O(log(n));
- 清除優先級隊列中所有節點
modCount++;
for (int i = 0; i < size; i++)
queue[i] = null;
size = 0;
}
清除優先級隊列中的所有節點。該操作的事件復雜度為:O(n);
- 迭代器
優先級隊列的迭代器并不保證遍歷按照指定的順序獲取節點元素。這是因為當在迭代器中執行remove操作時,可能會涉及到一個未訪問的元素被移動到了一個已經訪問過的節點位置(刪除操作時,當隊尾節點被放置到待移除節點位置的情況下,需要調用siftUp方法,siftUp(index, object)方法會升高待插入元素在樹中的位置index,直到待插入的元素大于或等于它待插入位置的父節點)。在迭代器操作中需要特殊處理。此時這些不幸的元素會在所有節點遍歷完后才得以遍歷。
附
- 證明“在平衡二叉樹中,葉子節點的個數總是大于等于前面所有非葉子節點個數之和。”
一個平衡二叉樹第N層節點數為:2^N
一個N層的平衡二叉樹總節點數為:2^(N+1) -1;
Sn = 2^0 + 2^1 + …… + 2^N
2*Sn = 2^1 + 2^2 + …… + 2^N + 2^(N+1)
將二個等式的左邊和右邊分別進行相減操作得到:
(2-1)Sn = 2^(N+1) - 2^0 ==> Sn = 2^(N+1) -1;
所以一個N層的二叉平衡樹除了葉子節點外的節點總數最大為:2^N -1;
因而2^N > 2^N -1,所以在滿平衡二叉樹下,葉子節點大于非葉子節點個數之和,當然在最后一層節點不滿的情況下,葉子節點依舊大于等于所有非葉子之和。
后記
若文章有任何錯誤,望大家不吝指教:)