一,介紹html
什麼是二項隊列,爲何會用到二項隊列?node
與二叉堆同樣,二項隊列也是優先級隊列的一種實現方式。在 數據結構--堆的實現之深刻分析 的末尾 ,簡單地比較了一下二叉堆與二項隊列。算法
對於二項隊列而言,它能夠彌補二叉堆的不足:merge操做的時間複雜度爲O(N)。二項隊列的merge操做的最壞時間複雜度爲O(logN)。數組
二項隊列的合併操做爲何是O(logN)?由於:對於N個結點的二項隊列,最多隻有logN棵二項樹。而合併操做就是合併兩棵高度相同的二項樹。(合併操做是指將二個二項隊列合併,合併這兩個二項隊列中高度相同的二項樹)數據結構
二,二項隊列的基本操做及實現post
在詳細介紹二項的隊列的基本操做以前,先了解下二項隊列這種數據結構:this
1)一個二項隊列是若干棵樹的集合。也就是說,二項隊列不只僅是一棵樹,而是多棵樹,而且每一棵樹都遵照堆序的性質,所謂堆序的性質,就是指每一個結點都比它的左右子樹中結點要小(小頂堆)。這棵樹稱爲「二項樹」spa
2)二項隊列中的樹高度不一樣,一個高度至多存在一棵二項樹。將高度爲0的二項樹記爲 B(0),高度爲 k 的二項樹記爲 B(k)指針
也就是說,對於k>=0,二項隊列中至多存在一棵 B(k)的二項樹。code
3)B(k)是由 B(0)、B(1)、B(2)....B(k-1)組成的。B(0)是一棵單節點(2^0 = 1)的樹,B(k)中含有 2^k 個結點。
高度爲 k 的二項樹B(k)經過將一棵二項樹B(k-1)附到另外一棵二項樹B(k-1)的根上構成。而B(k-1)又能夠由B(k-2)附到另外一棵B(k-2)的二項樹上,故正如上面提到,B(k)是由 B(0)、B(1)、B(2)....B(k-1)組成的。
4)具備N個結點的二項隊列最多有 logN 棵二項樹。由於,每棵二項樹B(k)中含有2^k個結點。
故:2^0 + 2^1 + 2^2 + .... + 2^k = N,獲得 k=logN。k爲樹的棵數。
注意到上面提到的是「最多」 有logN 棵二項樹,這說明一個二項隊列能夠缺乏某棵 B(i) , 0<=i<=k
5)由二項樹中結點個數的性質(B(k)有2^k個結點),而二項隊列又是若干二項樹的集合,故二項隊列能夠採用二進制數來標記:
如,大小爲13(共有13個結點)的二項隊列能夠用森林 B(3)、B(2)、B(0)表示,並能夠把這種表示寫成 1101,1101以二進制形式表示13,並且還表示了該二項隊列中,不存在B(1)這樣的樹。
介紹了二項隊列的性質或者說邏輯結構,如今介紹下二項隊列的存儲結構。
二項隊列是在內在中如何存儲的呢?(從網上找到一張圖以下:)
首先是須要一個一維數組。該數組中的每一個元素存儲一棵二項樹的根結點指針。好比,最右邊的那個數組元素存儲了一顆高度爲0的二項樹B(0)。B(0)只有一個結點,該結點的權值爲13。若是數組的長度表示二進制的位數,那麼這個二項隊列表示爲 00001101
這說明該二項隊列不存在高度爲七、六、五、四、1 這樣的二項樹:B(7)、B(6)、B(5)、B(4)、B(1)
此外,還能夠看出:
①數組大小爲二項樹的數目乘2加1,或者說二項樹的數目是數組的長度除以2再減去1。二項樹在數組中存儲是按高度排序的。
②數組第 i 號索引處,存儲的是高度爲 i 的二項樹。如,第0號索引,存儲高度爲0的二項樹,該二項樹只有一個結點,結點權值爲13
除了須要一維數組存儲各棵樹的根結點外,固然還須要保存各棵二項樹了,二項樹的採用的是鏈表 表示,這裏採用的是「左孩子右兄弟」表示法。
所以,二項隊列的實現類的結構以下:
1 public final class BinomialQueue<AnyType extends Comparable<? super AnyType>> 2 { 3 4 private static final int DEFAULT_TREES = 1; 5 6 private int currentSize; // # items in priority queue 7 private BinNode<AnyType> [ ] theTrees; // An array of tree roots 8 9 /** 10 * Construct the binomial queue. 11 */ 12 public BinomialQueue( ) 13 { 14 theTrees = new BinNode[ DEFAULT_TREES ]; 15 makeEmpty( ); 16 } 17 18 19 private static class BinNode<AnyType> 20 { 21 AnyType element; // The data in the node 22 BinNode<AnyType> leftChild; // Left child 23 BinNode<AnyType> nextSibling; // Right child 24 // Constructors 25 BinNode( AnyType theElement ) 26 { 27 this( theElement, null, null ); 28 } 29 30 //other operations..... 31 } 32 //other operations..... 33 }
第7行是一維數組,第19至23行是採用「左孩子右兄弟」表示法的結點的定義。
①merge操做
merge操做是合併二個二項隊列,合併二項隊列過程當中須要合併這兩個二項隊列中 高度相同的二項樹(後面的combineTrees()方法)
假設須要合併二項隊列H(1)和H(2),合併後的結果爲H(3)。合併兩個二項隊列的過程以下:
a)尋找H(1)和H(2)中高度相同的二項樹,調用combineTrees()合併這兩顆二項樹
b)重複 a) 直至樹中再也不有高度相同的二項樹爲止
代碼分析以下:
1 /** 2 * Return the result of merging equal-sized t1 and t2. 3 */ 4 private BinNode<AnyType> combineTrees( BinNode<AnyType> t1, BinNode<AnyType> t2 ) 5 { 6 if( t1.element.compareTo( t2.element ) > 0 ) 7 return combineTrees( t2, t1 );//第一個參數t1老是表明:根的權值較小的那顆二項樹 8 t2.nextSibling = t1.leftChild;//把權值大的二項樹的左孩子做爲權值小的二項樹的右兄弟 9 t1.leftChild = t2;//把權值小的二項樹 做爲 權值大的 二項樹 的 左孩子 10 return t1; 11 }
combineTrees()方法用來合併兩棵高度相同的二項樹,(注意是二項樹,而不是二項隊列)。樹採用的是左孩子右兄弟表示法。
第4行,t1是根的權值較小的二項樹樹,第8-9行,將根權值較大的那顆二項樹成爲根權值較小的二項樹(t1)的子樹,便可完成二項樹的合併。
二項隊列的合併,是二項隊列中高度相同的各個子樹之間的合併。
故merge操做的代碼以下(來自於《數據結構與算法分析Mark Allen Weiss》):
1 /** 2 * Merge rhs into the priority queue. 合併this 和 rhs 這兩個二項隊列 3 * rhs becomes empty. rhs must be different from this. 4 * @param rhs the other binomial queue. 5 */ 6 public void merge( BinomialQueue<AnyType> rhs ) 7 { 8 if( this == rhs ) // Avoid aliasing problems.不支持兩個相同的二項隊列合併 9 return; 10 11 currentSize += rhs.currentSize;//新合併後的二項隊列中的結點個數 12 13 if( currentSize > capacity( ) ) 14 { 15 int newNumTrees = Math.max( theTrees.length, rhs.theTrees.length ) + 1; 16 expandTheTrees( newNumTrees ); 17 } 18 19 BinNode<AnyType> carry = null; 20 for( int i = 0, j = 1; j <= currentSize; i++, j *= 2 ) 21 { 22 BinNode<AnyType> t1 = theTrees[ i ]; 23 BinNode<AnyType> t2 = i < rhs.theTrees.length ? rhs.theTrees[ i ] : null; 24 //合併分8種狀況 25 int whichCase = t1 == null ? 0 : 1; 26 whichCase += t2 == null ? 0 : 2; 27 whichCase += carry == null ? 0 : 4; 28 29 switch( whichCase ) 30 { 31 case 0: /* No trees */ 32 case 1: /* Only this */ 33 break; 34 case 2: /* Only rhs */ 35 theTrees[ i ] = t2; 36 rhs.theTrees[ i ] = null; 37 break; 38 case 4: /* Only carry */ 39 theTrees[ i ] = carry; 40 carry = null; 41 break; 42 case 3: /* this and rhs */ 43 carry = combineTrees( t1, t2 ); 44 theTrees[ i ] = rhs.theTrees[ i ] = null; 45 break; 46 case 5: /* this and carry */ 47 carry = combineTrees( t1, carry ); 48 theTrees[ i ] = null; 49 break; 50 case 6: /* rhs and carry */ 51 carry = combineTrees( t2, carry ); 52 rhs.theTrees[ i ] = null; 53 break; 54 case 7: /* All three */ 55 theTrees[ i ] = carry; 56 carry = combineTrees( t1, t2 ); 57 rhs.theTrees[ i ] = null; 58 break; 59 } 60 } 61 62 for( int k = 0; k < rhs.theTrees.length; k++ ) 63 rhs.theTrees[ k ] = null;//合併完成以後,釋放rhs內存 64 rhs.currentSize = 0; 65 }
重點介紹下二項隊列合併爲何會有8種狀況:
第25至27行,這8種狀況能夠用三個二進制位來表示:
//合併分8種狀況 int whichCase = t1 == null ? 0 : 1; whichCase += t2 == null ? 0 : 2; whichCase += carry == null ? 0 : 4;
0<=whichCase<=7,一共8種狀況。只分析一種狀況,其餘狀況相似分析:
分析有rhs和this的狀況。即,須要將 this 和 rhs 這兩棵二項樹合併,this表明當前二項樹。二進制表示爲 011
t1(this)不爲空,whichCase=1,而後 t2爲rhs,也不爲空,故whichCase再加2。這裏加2的緣由是rhs是二進制中的第2位。
situation carry rhs this
no trees 0 0 0
only this 0 0 1
only rhs 0 1 0
only carry 1 0 0
this and rhs 0 1 1
.....
.....
All 1 1 1
carry表示上一步合併二項樹過程上,生成的一棵新二項樹。
肯定了哪一種合併狀況後,再來看看對這些狀況是如何處理的:
case 0: /* No trees */ case 1: /* Only this */ break;
第0種狀況,表示沒有樹須要合併。第1種狀況表示,只有this (當前二項樹)樹。什麼叫只有當前二項樹呢?(引用網友的一張圖:)
黃色節點表示的是二項隊列H(1),綠色節點表示的二項隊列H(2),紅色節點表示合併二項隊列H(1)和H(2)以後,生成的新的二項隊列H(3)。
H(2)中有一棵節點權值爲13的高度爲1的二項樹,而H(1)中沒有高度爲1的二項樹。此時就是rhs == null。即只有當前二項樹(this樹)
再來分析下case3狀況:
case 3: /* this and rhs */ carry = combineTrees( t1, t2 ); theTrees[ i ] = rhs.theTrees[ i ] = null; break;
如上圖,H(1)中有一棵根爲12高度爲1的二項樹;H(2)中也有一棵高度爲1,但根爲14的二項樹。此時this 和 rhs 都不爲null。
調用combineTress(t1,t2)方法合併成一棵新的二項樹,該二項樹高度爲2,用carray表示。這也是上面提到的」 carry表示上一步合併二項樹過程上,生成的一棵新二項樹。「
生成carry以後,H(1)和H(2)中都已經沒有高度爲1的二項樹了,所以執行: theTrees[ i ] = rhs.theTrees[ i ] = null;
再來分析下case7狀況:
case 7: /* All three */ theTrees[ i ] = carry; carry = combineTrees( t1, t2 ); rhs.theTrees[ i ] = null; break;
仍是參考上面圖:H(1)、H(2)在執行了case3以後,這二個二項隊列一共有三棵高度爲2的二項樹了。
第一棵是:case3中生成的。它的根結點的權值爲14
第二棵是:H(1)中原來存在的。它的根結點的權值爲12
第三棵是:H(2)中原來存在的。它的根結點的權值爲23
所以,whichCase的值爲7=1+2+4
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
代碼中對該狀況的處理是這樣的(代碼處理與圖中畫的有點不同:圖中畫的是將兩棵根的權值較小的二項樹(第一棵和第二棵)合併 而代碼中合併的是第二棵和第三棵。
也就是說,當有三棵高度相同的二項樹時,其中一棵是上一步合併生成的carray,另外兩棵是原來二項隊列中存在的。並非把其中兩棵根權值較小的二項樹進行合併,而是合併原來二項隊列中存在的那兩棵:carry = combineTrees( t1, t2 );總之,在進行合併時,合併的規則並非:選擇兩棵根的權值較小的二項樹合併。而是根據代碼中的case狀況來進行合併。
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
數組位置 i 處 保存上一步生成的高度爲2的二項樹。 theTrees[ i ] = carry;
合併原來存在的那兩棵高度爲2的二項樹, carry = combineTrees( t1, t2 );
合併以後,釋放rhs佔用的空間, rhs.theTrees[ i ] = null;
至此,合併操做分析完畢,其餘狀況的合併相似於上面的分析。
②insert 操做
insert操做能夠看做是特殊的合併操做。即rhs二項隊列中只有一棵高度爲0的二項樹。插入操做的複雜度與是否存在高度爲 i 的二項樹有關,具體分析參考Mark Allen Weiss的書籍。平均狀況下的時間複雜度爲O(1)。
代碼以下:
1 /** 2 * Insert into the priority queue, maintaining heap order. 3 * This implementation is not optimized for O(1) performance. 4 * @param x the item to insert. 5 */ 6 public void insert( AnyType x ) 7 { 8 merge( new BinomialQueue<>( x ) ); 9 }
③deleteMin操做
deleteMin操做的步驟以下:
1)尋找一棵具備最小權值的根的二項樹,設爲B(i)。
int minIndex = findMinIndex( );
AnyType minItem = theTrees[ minIndex ].element;
BinNode<AnyType> deletedTree = theTrees[ minIndex ].leftChild;
2)刪除B(i)的根,獲得若干棵二項樹:B(0)、B(1)...B(i-1)。這些二項樹組成一個新的二項隊列 H''
// Construct H'' BinomialQueue<AnyType> deletedQueue = new BinomialQueue<>( ); deletedQueue.expandTheTrees( minIndex + 1 ); deletedQueue.currentSize = ( 1 << minIndex ) - 1; for( int j = minIndex - 1; j >= 0; j-- ) { deletedQueue.theTrees[ j ] = deletedTree; deletedTree = deletedTree.nextSibling; deletedQueue.theTrees[ j ].nextSibling = null; }
3)原來的二項隊列,刪除B(i)這棵根的權值最小的二項樹後,獲得的新的二項隊列 H'
// Construct H' theTrees[ minIndex ] = null; currentSize -= deletedQueue.currentSize + 1;
4)合併 H'' 和 H' 便可
merge( deletedQueue );
整個deleteMin的實現代碼以下:
/** * Remove the smallest item from the priority queue. * @return the smallest item, or throw UnderflowException if empty. */ public AnyType deleteMin( ) { if( isEmpty( ) ) throw new UnderflowException( ); int minIndex = findMinIndex( ); AnyType minItem = theTrees[ minIndex ].element; BinNode<AnyType> deletedTree = theTrees[ minIndex ].leftChild; // Construct H'' BinomialQueue<AnyType> deletedQueue = new BinomialQueue<>( ); deletedQueue.expandTheTrees( minIndex + 1 ); deletedQueue.currentSize = ( 1 << minIndex ) - 1; for( int j = minIndex - 1; j >= 0; j-- ) { deletedQueue.theTrees[ j ] = deletedTree; deletedTree = deletedTree.nextSibling; deletedQueue.theTrees[ j ].nextSibling = null; } // Construct H' theTrees[ minIndex ] = null; currentSize -= deletedQueue.currentSize + 1; merge( deletedQueue ); return minItem; }
三,二項隊列與二叉堆的比較
基本操做: insert(平均狀況下) deleteMin merge
二項隊列: O(1) O(logN) O(logN)
二叉堆: O(1) O(logN) O(N)
可見,二項隊列有效地支持了merge操做。
可是,須要注意的是:二項隊列的實現用到了鏈表,樹中的每一個元素存儲在一個結點中,結點之間採用「左孩子右兄弟」表示法進行連接,此外還須要一個額外的數組來保存各棵二項樹的根結點,存儲開銷要比二叉堆大。
而對於二叉堆的存儲,則簡單得多。它只須要一個一維數組便可存儲各個元素。
四,參考資料
http://www.cnblogs.com/pacoson/p/5151886.html
《數據結構與算法分析 JAVA語言描述第三版》Mark Allen Weiss著