一,介紹
什么是二項隊列,為什么會用到二項隊列?
與二叉堆一樣,二項隊列也是優先級隊列的一種實現方式。在 數據結構--堆的實現之深入分析 的末尾 ,簡單地比較了一下二叉堆與二項隊列。
對於二項隊列而言,它可以彌補二叉堆的不足:merge操作的時間復雜度為O(N)。二項隊列的merge操作的最壞時間復雜度為O(logN)。
二項隊列的合並操作為什么是O(logN)?因為:對於N個結點的二項隊列,最多只有logN棵二項樹。而合並操作就是合並兩棵高度相同的二項樹。(合並操作是指將二個二項隊列合並,合並這兩個二項隊列中高度相同的二項樹)
二,二項隊列的基本操作及實現
在詳細介紹二項的隊列的基本操作之前,先了解下二項隊列這種數據結構:
1)一個二項隊列是若干棵樹的集合。也就是說,二項隊列不僅僅是一棵樹,而是多棵樹,並且每一棵樹都遵守堆序的性質,所謂堆序的性質,就是指每個結點都比它的左右子樹中結點要小(小頂堆)。這棵樹稱為“二項樹”
2)二項隊列中的樹高度不同,一個高度至多存在一棵二項樹。將高度為0的二項樹記為 B(0),高度為 k 的二項樹記為 B(k)
也就是說,對於k>=0,二項隊列中至多存在一棵 B(k)的二項樹。
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
這說明該二項隊列不存在高度為7、6、5、4、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著