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著

  • 相关阅读:
    JDBC值事务
    JDBC之存储过程
    2016年计划要看的书,收集来自爱学习爱阅读的精英们
    step by step设置postgresql用户密码并配置远程连接
    在CentOS上搭建svn服务器及注意事项
    我为儿子开发的第一款Android App,用于九九乘法练习
    C语言实现九九乘法口诀表
    selenium-自动化测试51job网站(MacOS + Safari)2020年10月6日
    Oracle中row_number()、rank()、dense_rank() 的区别
    SQL错题集
  • 原文地址:https://www.cnblogs.com/hapjin/p/5468817.html
Copyright © 2011-2022 走看看