在業務場景中,處理一個任務佇列,可能需要依照某種優先順序順序,這時,Java中的PriorityQueue(優先佇列)便可以派上用場。優先佇列的原理與堆排序密不可分,可以參考我之前的一篇部落格:
原理
PriorityQueue中維護一個Queue[]陣列,在邏輯上把它理解成一個小根堆或大根堆,即一個完全二叉樹,每一個三元組中父節點小於兩個孩子結點(小根堆,如果是大於則是大根堆)。本部落格以小根堆來進行說明,因為PriorityQueue預設實現小根堆,即小的數先出隊,當然也可以自定義Comparator實現大根堆。
- 入隊:每次入隊時,把新元素掛在最後,從下往上遍歷調整成小根堆;
- 出隊:每次出隊時,移除頂部元素,把最後的元素移到頂部,並從上往下遍歷調整成小根堆。
出隊
poll()方法如下:
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;
}
可以看到,隊首元素 queue[0] 出隊,隊尾的元素 queue[s] 進入 siftDown(0, x) 方法進行堆調整。siftDown方法如下:
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
//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;//找到了key應該放入的位置
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;
}
可以看到,這與堆排序中的堆調整如出一轍。
入隊
offer方法如下所示:
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;
}
同樣,其核心在於 siftUp(i, e) 方法。如下所示:
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等於size
k = parent;
}
queue[k] = key;//最後在待插入位置賦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;
}
此方法,是一個不斷從父節點往子節點賦值的過程,直到找到適合放置插入結點值的位置。
移除
removeAt 方法如下所示:
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;
}
移除下標為i的元素,相當於以 i 為根節點的完全二叉樹的出隊,於是執行 siftDown 方法調整最後一個元素 moved 的位置,即將該堆調整為小根堆。調整完之後,如果 moved 沒有來到 i 的位置,說明 i 以上的堆結構一定符合規則;如果 moved 被調整到 i 位置,i上面的父節點有可能比 moved大,所以需要 siftUp(i, moved) 方法從 i 位置向上調整,調整為小根堆,完畢。
總結
其實不管是 siftUp 方法還是 siftDown 方法,都是利用了完全二叉樹的性質,通過父節點與孩子結點之間的快速訪問來實現的。