數據結構--二項隊列分析及實現

一,介紹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著

相關文章
相關標籤/搜索