二叉堆與Java中的優先隊列


之前在A*算法演示程序的編碼過程中,發現javaScript并沒有原生的優先隊列,于是去Java中找到了PriorityQueue類,研究了一下源碼。Java中的優先隊列基于最小二叉堆實現。最小二叉堆具有兩個性質:

  • 結構性:必須是一顆完全二叉樹,樹的插入從左到右,一棵完全二叉樹的高度為小于log(N)的最大整數。

  • 堆序性:二叉樹的父節點必須小于兩個子節點。父節點下標為n,兩個子節點下標為2n+1和2(n+1)。父節點的值總是小于子節點,兩個子節點間大小關系不確定。

Java PriorityQueue特點:

  • 不接受null值

  • 不接受不可排序的值

  • 線程不安全

  • 容量沒有上限

  • 插入和刪除元素的時間復雜度為O(log(n))

首先來看下最關鍵的兩個方法,siftDown(int k, E x)和siftUp(int k, E x)。這兩個方法是插入、刪除、排序和初始化操作的基礎。兩個方法的目的都是讓節點k所在的子樹符合二叉堆的性質,堆序性。區別在于,siftDown將節點k作為子樹的父節點處理,siftUp則將節點k作為子樹的子節點處理。

siftDown


private void siftDownComparable(int k, E x) {        Comparablekey = (Comparable)x;        int half = size >>> 1;        //k) 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;    }~~~關于這部分代碼,比較有意思的是迭代條件`k>>1`,我們分size為奇數和偶數兩種情況來看:* 奇數:size為奇數,`half=(size-1)/2`,最后一個節點下標為`size-1`,父節點下標為`(size-1)/2-1`,所以`kkey = (Comparable) 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;

}

siftUp的代碼很容易理解,遞歸的與父節點比較大小,然后根據結果決定是否需要對調位置。

heapify

接下來是PriorityQueue的初始化方法中最復雜的一個,從一個無需集合(Collection)生成一個優先隊列。


private void initFromCollection(Collection c) {

initElementsFromCollection(c);

heapify();

}

initElementsFromCollection(c)方法很簡單,將集合c中的數據放入隊列,將集合c的大小賦予size屬性。而heapify()方法就是將集合c轉換為二叉堆的關鍵。


private void heapify() {

for (int i = (size >>> 1) - 1; i >= 0; i--)

siftDown(i, (E) queue[i]);

}

size>>>1的特殊性我們剛才已經討論過了,這里看下heapify()是怎么做的,從(size>>>1)-1開始向上遍歷,對每一個父節點,都要保證它所在的子樹符合條件,那么整棵樹都將符合條件。

add offer

向隊列中添加元素,兩者代碼相同,之所以并存是因為PriorityQueue繼承自Queue。


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方法進行調整。最壞的情況,新插入的元素比根節點的元素還要小,那么siftUp要執行到根節點所在的子樹。

removeAt

刪除隊列中下標為i的元素。由于要保持完整二叉樹的結構,刪除元素后仍需要進行重新排序。


private E removeAt(int i) {

modCount++;

int s = --size;

//如果i就是最后一個節點的下標,直接刪除

if (s == i)

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;

}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容