在Java SE 5.0中,引入了一些新的Collection API,PriorityQueue就是其中的一個。今天由於機緣巧合,花了一個小時看了一下這個類的內部實現,代碼很有點意思,所以寫下來跟大家分享一下。從中也可以看到,Java源代碼的OpenSource對於我們程序員編程帶來了多大的幫助。
最初的起因是我閱讀文檔不仔細,使用PriorityQueue出現了問題。我剛開始只是把它當作一個一般的FIFO實現來使用,結果發現poll()的結果跟我想象的不一樣,後來才發現,PriorityQueue會對入隊的元素進行排序,所以在隊列頂端的總是最小的元素。
有趣的是,我在仔細閱讀文檔以前,曾經用調試器察看了我的PriorityQueue,所以即便我後來閱讀文檔知道了它的正確行爲,卻發現內部實現似乎跟我想象的不同。把問題簡化成下面的代碼:
public static void main(String[] args) { |
輸出的結果如下:
apple boy fox easy dog
pq.poll(): apple
boy dog fox easy
pq.poll(): boy
dog easy fox
pq.poll(): dog
easy fox
pq.poll(): easy
fox
pq.poll(): fox
可以看到,雖然PriorityQueue保持了隊列頂部元素總是最小,但內部的其它元素的順序卻隨着元素的減少始終處於變化之中。由於沒有總結出有效的規律,我決定察看源代碼來一探究竟。從Netbeans中非常方便的連接到PriorityQueue的add函數實現,最終跟蹤到函數private void siftUpComparable(int k, E x),定義如下:
private void siftUpComparable(int k, E x) { |
相對於add的操作,該函數的入口參數k是指新加入元素的下標,而x就是新加入的元素。乍一看,這個函數的實現比較令人費解,尤其是parent的定義。通過進一步分析瞭解到,PriorityQueue內部成員數組queue其實是實現了一個二叉樹的數據結構,這棵二叉樹的根節點是queue[0],左子節點是queue[1],右子節點是queue[2],而queue[3]又是queue[1]的左子節點,依此類推,給定元素queue[i],該節點的父節點是queue[(i-1)/2]。因此parent變量就是對應於下標爲k的節點的父節點。
弄清楚了這個用數組表示的二叉樹,就可以理解上面的代碼中while循環進行的工作是,當欲加入的元素小於其父節點時,就將兩個節點的位置交換。這個算法保證瞭如果只執行add操作,那麼queue這個二叉樹是有序的:該二叉樹中的任意一個節點都小於以該節點爲根節點的子數中的任意其它節點。這也就保證了queue[0],即隊頂元素總是所有元素中最小的。
需要注意的是,這種算法無法保證不同子樹上的兩個節點之間的大小關係。舉例來說,queue[3]一定會小於queue[7],但是未必會小於queue[9],因爲queue[9]不在以queue[3]爲根節點的子樹上。
弄清楚了add的操作,那麼當隊列中的元素有變化的時候,對應的數組queue又該如何變化呢?察看函數poll(),最終追中到函數private E removeAt(int i),代碼如下:
private E removeAt(int i) { |
這個函數的實現方法是,將隊尾元素取出,插入到位置i,替代被刪除的元素,然後做相應的調整,保證二叉樹的有序,即任意節點都是以它爲根節點的子樹中的最小節點。進一步的代碼就留給有興趣的讀者自行分析,要說明的是,對於queue這樣的二叉樹結構有一個特性,即如果數組的長度爲length,那麼所有下標大於length/2的節點都是葉子節點,其它的節點都有子節點。
總結:可以看到這種算法的實現,充分利用了樹結構在搜索操作時的優勢,效率又高於維護一個全部有序的隊列。