二项堆的定义
二项堆(binomial heap)又称二项队列(binomial queue),是由若干棵堆序树,称之为二项树(binomial tree)组成的集合,即由若干棵二项树组成的森林。在一个二项堆中,满足所有二项树的大小都是二的次幂,且任意两棵二项树的大小互不相同。
对于二项树的定义如下:设对于大小为 2k−1 二项堆 { B0 , B1 , … , Bk−1 },二项树 B0 为单节点,二项树 Bi(1≤i≤k−1) 以一个单节点为根,以 i 棵二项树 B0 , B1 ,…, Bi−1 为子树构成的堆序树。任意一棵二项树都是一个堆。在代码中,二项树一般由左儿子右兄弟的方法表示。为了后文合并算法及删除最值算法正确运行,二项树中的每个节点的子节点指针指向最大的儿节点,兄弟节点指针指向规模为原节点规模一半的兄弟节点。
根据定义,若将一个二项堆的大小用二进制表示,则该二项堆由其二进制表示下相应数位为 1 的相应大小的二项树组成。与朴素的二叉堆不同的是,二项堆不仅支持单元素插入和单最值查询删除维护,而且支持在对数时间内两个任意大小的二项堆合并操作。二项树具有如下性质:高度为 k 的二项树在深度 d 有 (kd) 个节点。
对于二项堆的定义代码如下:
#define NIL -1
struct BINOMIAL_HEAP_NODE
{
int pow,sbl,son;
}BHData[MAX_N];
struct BINOMIAL_HEAP
{
int BiTree[MAX_T],BHSize;
}BiHeap[MAX_H];
二项堆的单元素插入维护
二项堆没有特别的插入算法,一般是将单元素作为一个大小为 1 的二项堆与原二项堆合并。二项堆的单元素插入维护的摊还时间复杂度为 O(1),即在二项堆中插入元素的时间是常数级别的。这一点并不显然,因为单次插入维护的最坏时间复杂度可能达到对数级别。但是,从整体来看,由于最坏情况出现次数少,所以最坏仍然是常数级别。
对二项堆单元素插入维护的时间分析,简单来说,我们可以将其归约为二进制计数器递增问题,该问题已被证明是摊还常数级别,具体方法详见《算法导论》。具体来说,用势能分析法分析,设一个二项堆的势能为该状态下二项树的个数,即可用储存的势能来补偿进位时的时间消耗。
对于二项堆的单元素插入维护代码如下:
int BH_Memory[MAX_N],BH_MemTop=0;
int BH_NewNode()
{
if(BH_Memory[0]>0)
return BH_Memory[BH_Memory[0]--];
return BH_MemTop++;
}
void BH_Insert(BINOMIAL_HEAP &bh0,int num)
{
BINOMIAL_HEAP bht;
bht.BHSize=1;
bht.BiTree[0]=BH_NewNode();
BHData[bht.BiTree[0]].pow=num;
BHData[bht.BiTree[0]].sbl=BHData[bht.BiTree[0]].son=NIL;
bh0=BH_Merge(bh0,bht);
}
二项堆的单最值查询删除维护
由于二项堆是堆的集合,所以不能确定二项堆中最值的位置,与朴素的二叉树不同,就算是查询最值也需要对数级别时间。找到最值所在的二项树后,我们将最值从二项树中删除,并将该二项树根节点的所有子树提取出来,放入到一个新二项堆中,与原二项堆合并即可。
二项堆的单最值查询删除维护的时间复杂度为 O(logn),这点不难证明。将子树构建成新二项堆的操作需要指向兄弟的指针所构成的兄弟节点链按子树大小有序排列,至于为何按从大到小排列而并非从小到大排列,这点将在后文二项树的合并中加以说明。
对于二项堆的单最值查询删除维护代码如下:
int BH_FindMin(BINOMIAL_HEAP bh0)
{
int mini=0;
for(int i=1;i<bh0.BHSize;i++)
if(bh0.BiTree[mini]==NIL||bh0.BiTree[i]!=NIL
&&BHData[bh0.BiTree[i]].pow<BHData[bh0.BiTree[mini]].pow)
mini=i;
return mini;
}
int BH_DeleteMin(BINOMIAL_HEAP &bh0)
{
if(bh0.BHSize==0)
return NIL;
int mini=BH_FindMin(bh0),minum=bh0.BiTree[mini];
BINOMIAL_HEAP bht;
bht.BHSize=mini;
for(int i=mini-1,j=BHData[minum].son;j!=NIL;i--,j=BHData[j].sbl)
bht.BiTree[i]=j;
for(int i=BHData[minum].son,j=BHData[i].sbl;j!=NIL;i=j,j=BHData[j].sbl)
BHData[i].sbl=NIL;
BH_Memory[++BH_Memory[0]]=minum;
bh0.BiTree[mini]=NIL;
if(bh0.BHSize==mini+1)
bh0.BHSize--;
bh0=BH_Merge(bh0,bht);
return BHData[minum].pow;
}
二项堆的合并维护
二项堆的合并维护看起来很像两个二进制数的算术加操作。按二项树从小到大进行合并操作,必要时存在进位的二项树。将二进制数的算术加类比,便可以很容易理解二项堆的合并维护。任意两棵大小相等的二项树的合并都是常数级别时间,所以二项堆的合并维护的时间复杂度为 O(logn)。
两棵二项树的合并操作只需要常数级别时间,具体操作是将根节点权值大的二项树作为根节点权值小的二项树的子树。在这里,若插入到兄弟链末尾则需对数时间查询插入位置,而插入到兄弟链起始则只需要常数时间。这样,每棵二项树中节点的子树兄弟指向顺序都是从大到小其实是二项树合并的必然结果。
对于二项堆的合并维护代码如下:
#define max(a,b) \
({ \
typeof(a) __tmp_a=(a); \
typeof(b) __tmp_b=(b); \
(void)(&__tmp_a==&__tmp_b); \
__tmp_a>__tmp_b?__tmp_a:__tmp_b; \
})
int BT_Combine(int &bt1,int &bt2)
{
if(BHData[bt1].pow>BHData[bt2].pow)
return BT_Combine(bt2,bt1);
int bt0=bt1;
BHData[bt2].sbl=BHData[bt1].son;
BHData[bt1].son=bt2;
bt1=bt2=NIL;
return bt0;
}
BINOMIAL_HEAP BH_Merge(BINOMIAL_HEAP &bh1,BINOMIAL_HEAP &bh2)
{
BINOMIAL_HEAP bh0;
int btt=NIL;
bh1.BiTree[bh1.BHSize]=NIL;
for(int i=bh1.BHSize+1;i<=bh2.BHSize;i++)
bh1.BiTree[i]=NIL;
bh0=bh1;
bh0.BHSize=max(bh1.BHSize,bh2.BHSize);
for(int i=0;i<=bh0.BHSize;i++)
switch((bh1.BHSize>i&&bh1.BiTree[i]!=NIL)
+2*(bh2.BHSize>i&&bh2.BiTree[i]!=NIL)
+4*(btt!=NIL))
{
case 0:
case 1:break;
case 2:bh0.BiTree[i]=bh2.BiTree[i],bh2.BiTree[i]=NIL;break;
case 3:btt=BT_Combine(bh0.BiTree[i],bh2.BiTree[i]);bh0.BiTree[i]=bh2.BiTree[i]=NIL;break;
case 4:bh0.BiTree[i]=btt;btt=NIL;break;
case 5:btt=BT_Combine(btt,bh0.BiTree[i]);bh0.BiTree[i]=NIL;break;
case 6:
case 7:btt=BT_Combine(btt,bh2.BiTree[i]);bh2.BiTree[i]=NIL;break;
}
bh1.BHSize=bh2.BHSize=0;
if(bh0.BiTree[bh0.BHSize]!=NIL)
bh0.BHSize++;
return bh0;
}
二项堆的总结
二项堆作为可合并堆中思想较为简单的数据结构,是较为基础的。在二项堆中使用了倍增思想,并使整个数据结构维持在较为有序的状态。关于二项堆的单元素插入维护中用到的摊还分析,由于单最值查询删除维护和合并维护都花费对数级别时间,所以完全可以支付整个二项堆的势能变化。虽然时间复杂度仍不尽人意,但比起朴素的做法效率还是有优化之处的。