zoukankan      html  css  js  c++  java
  • 数据结构--二项队列分析及实现

    一,介绍

    什么是二项队列,为什么会用到二项队列?

    与二叉堆一样,二项队列也是优先级队列的一种实现方式。在 数据结构--堆的实现之深入分析 的末尾 ,简单地比较了一下二叉堆与二项队列。

    对于二项队列而言,它可以弥补二叉堆的不足: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著

  • 相关阅读:
    什么样的代码称得上是好代码?
    九年程序人生 总结分享
    Docker入门 第一课 --.Net Core 使用Docker全程记录
    阿里云 Windows Server 2012 r2 部署asp.net mvc网站 平坑之旅
    Visual studio 2015 Community 安装过程中遇到问题的终极解决
    Activiti6.0 spring5 工作流引擎 java SSM流程审批 项目框架
    java 进销存 库存管理 销售报表 商户管理 springmvc SSM crm 项目
    Leetcode名企之路
    24. 两两交换链表中的节点
    21. 合并两个有序链表
  • 原文地址:https://www.cnblogs.com/hapjin/p/5468817.html
Copyright © 2011-2022 走看看