可合并堆1:二项堆(Binominal Heap)

二项堆是一种可合并队,其合并的复杂度为O(log(n)+log(m))。二项堆与二项树有着天然的联系,解析二项堆之前,先解析二项树。

1k阶二项树B_{k}是一个递归定义:

(1)单个节点是一颗二项树,表示为B_{0}

(2)二项树B_{k}由两颗B_{k-1}二项树组成

(3)像最小二项树一样,两颗B_{k-1}二项树组成一个B_{k},其中一个根节点较小二项树的根形成新树的根节点,另外一个作为其子树。如下图所示均是k阶二项树。

上图分别是B_{0}B_{1}B_{2}。由定义可知,k阶二项树B_{k}的节点的数量和必为偶数,且每颗k阶二项树B_{k}B_{0}B_{1}B_{2}...B_{k-1}组成,k指的是根节点下孩子节点的数量。而最右边的图所示不是k阶二项树。由此可以导出二项树有如下性质

性质1:k阶二项树B_{k}的总节点数量为2^{k}

归纳法可证。当k=0时,B_{1}节点数量的为2^{0}=1,假设B_{k-1}的节点数量为2^{k-1,而B_{k}是两颗B_{k-1}组成的,所以B_{k-1}的节点数量为2^{k-1+2^{k-1=2^{k},性质1得证。

性质2:二项树的高度为k(高度定义根节点到最远叶子节点上的简单路径上边的数目)

归纳法可证。当k=0时,B_{0}的高度为0,假设二项树B_{k-1}的高度为k-1,由于B_{k}是由两颗B_{k-1}合并而得,且合并后其中一颗B_{k-1}作为了新树根的左子树,所以新树B_{k}的高度=B_{k-1}的高度+1=k-1+1,性质2得证。

性质3::在高度为i,i=0,1,2...k的层次上,有\binom{k}{i}(即C_{k}^{i})个节点

当k=1时,显然成立。假设B_{k-1}上第i层上节点数目C_{k-1}^{i}B_{k}是由两颗B_{k-1}合并而得,合并后其中一颗B_{k-1}作为了新树根的左子树,合并后新树第i层节点的数量由原B_{k-1}的第i层的节点和作为左子树的B_{k-1}i-1层节点的数量,设为total,即total=C_{k-1}^{i}+C_{k-1}^{i-1}=\frac{(k-1)!}{i!(k-i-1)!}+\frac{(k-1)!}{(i-1)!(k-i)!}=\frac{(k-1)!}{(i-1)!(k-i-1)!}(\frac{1}{i}+\frac{1}{k-i})=\frac{k!}{i!(k-i)!}=C_{k}^{i}所以二项树B_{k}的第i层上的节点数量为C_{k}^{i}

性质4:根节点的度(一个节点的度为一个节点的孩子数目)为k,如果根节点下孩子从左到右编号为k-1,k-2,...0,则孩子节点i对应的是二项树B_{i}

根据二项树B_{k}的组成定义,可得到性质4

由于二项树的节点数必为偶数,作为优先队列的选择,不能满足奇数是个致命缺陷。那如何才能满足奇数呢,可不可以把不同度数的二项树“组合”起来,便可以满足奇数的要求,而且还可以满足堆性质,满足作为优先队列的条件。这些二项树“组合”起来的数据结构便是二项堆。

2 单个二项堆是由二项树组成的森林,森林中每颗二项树的度是唯一的,也就是有如下性质。

(1)在二项堆中,对每个k\geq 0,最多只有1个度为k的二项树

(2)二项堆中的节点数量n与二进制数n有一定关系,具体如下图所示。为如下序列构建二项堆

从上述过程,可以观察到如下结论

(1)上述例子,节点n=8,其二进制为1000,对应位上即B_{3}=1,B_{2}=0,B_{1}=0,B_{0}=0,代表此二项堆只有一颗B_{3}的二项树

(2)如果n=11,其二进制为1011,对应位上B_{3}=1,B_{2}=0,B_{1}=1,B_{0}=1,由三颗二项树组成B_{3}B_{1}B_{0}组成。而对于每个数n,有其唯一的二进制表达式,所示对节点数为n的二项堆,其组成是唯一的。

性质3:二项堆有n个节点,其高度不大于\left \lfloor log(n) \right \rfloor

二项堆的高度取决于其森林中第一颗二项树的高度,也就是n转化的二进制数中的首位‘1’代表的二项树,当森林中只有一颗二项树时,高度最大,为log(n),所以二项堆的高度不大于\left \lfloor log(n) \right \rfloor

性质4:二项堆有n个节点,其长度不大于\left \lfloor log(n) \right \rfloor+1

二项堆的长度定义二项堆森林中二项树的个数,也就是节点数n转化为二进制数中数位‘1’的个数。对于一个数n,其转化为二进制后的位数为\left \lfloor log(n) \right \rfloor+1,其中数位‘1’的个数必然不大于\left \lfloor log(n) \right \rfloor+1

2.1二项堆的存储

二项堆有如下三个指针

(1)父节点指针,指向此节点的父节点

(2)左儿子指针:指向最左的孩子

(3)右兄弟指针:指向此节点的右兄弟

如上图所示,最上层的是根节点链表,横向指针是右兄弟指针,向下的是左儿子指针,向上的是父节点指针,

2.2二项堆的操作如下

2.1.1· 合并操作

(1)待合并的2个二项堆森林合并一个二项堆森林

(2)新的二项堆执行合并过程(consolidation)。即按照度数从小到大,将度数一样的二项树进行合并,直至森林中不存在度数一样的二项树为止。

合并过程类似于二进制中的加法。如下图所示。

如上图所示,两颗二项堆的合并,从各自度数最小的二项树开始合并,如两个B_{0}合并成一个B_{1},两个B_{1}合并一个B_{2},如果两个堆B_{2}都存在,则进位过来的B_{2}保留,原堆上的两个B_{2}合并成B_{3},如果对于B_{i},如果两个堆只有一个堆存在B_{i},则保留B_{i},如果两个堆都不存在B_{i},则不保留。

合并操作的伪代码如下:

def merge(A,B)  //first 指针指向二项堆中最左侧的二项树
{
C=NULL
for(int i=0;i<A.degree()||i<B.degree();i++) //度数从小到大进行合并
    {
    if(tree(A,i)!=NULL)
         C.add(tree(A.i)) //将二项堆A中度为i的二项树加入新堆。
    if(tree(B,i)!=NULL) 
         C.add(tree(B.i)) 
    }
consolidation(C)
return C
}
def consolidation(C)
{
while(C exists the same degree tree) //存在度相同的二项树
{
for(int i=0;i<C.degree();i++) 
    {
    if(degree(x)==degree(y)) //两颗二项树度相同
        {
         if x.key<y.key
                swap(x,y) 
           make y leftchild of x //将其中一颗作为另一颗的左子树
           increase x.degree           
    }
}
}

二项堆的合并与每个二项堆的长度成正比,而长度不大于\left \lfloor log(n) \right \rfloor+1,所以合并节点数为n和m的二项堆的时间复杂度为\left \lfloor log(n) \right \rfloor+1+\left \lfloor log(m) \right \rfloor+1=O(log(n)+log(m))

2.1.2 插入操作

插入一个元素,将一个元素视为具有一个元素的二项树(B_{0}),然后看作具有一个元素的二项堆与原有堆的合并,其伪代码如下:

def insert(A,x){   //在堆A插入节点x
B=MakeintoHeap(x)
return marge(A,B)
}

而合并的时间复杂度为O(log(n)+log(m)),一个节点的二项堆的度为0,所以插入操作的复杂度为O(log(n))

2.1.3二项堆的构建

(1)二项堆的构建如本章开始部分的话,则是逐个插入法,其伪代码如下:

def create(queue q)
{
T=NULL
while(q!=null)
    {
    x=q.pop()
    insert(x,T)
    }
return T
}

用上述方法,其构建复杂度为lg1+lg2+...+lg(n-1),而根据stirling公式lgn!\doteq eq \frac{lg2\pi n}{2}+nlg\frac{n}{e},所以构建复杂度为O(nlg(n))

因为二项堆是可合并堆,可以采用合并法建堆。合并法的步骤是度数从0开始,按照顺序将度数相同的二项树两两合并,直至队列中不存在度数相同的二项树为止(此时队列中每个元素视为一颗二项树)。其实现过程如下:

合并法建堆的伪代码如下:

def create(list q)
{
for(int i=0;i<log(q.length);i++)  //度从0开始
{
if(x.degree==i)//找到第一个度为i的节点
    {
    for(y=x->next,y!=NULL;y=y->next)
        {
        if(y.degree==i)
            {
                merge(x,y)//找到度相同的两个节点,进行合并
                x=y->next
                break;
            }
        }
    }
}
return q
}

合并法建堆的时间复杂度如下。首先是对只有1节点(度为0)的队列进行合并\frac{n}{2}次,其次对2个节点(度为1)的左偏树进行合并\frac{n}{4}次,再次是4个节点(度为2)的左偏树进行合并\frac{n}{8}次,...2^{i-1}个节点的左偏树合并\frac{n}{2^{i}}次,...最后对\frac{n}{2}n-\frac{n}{2}个节点(\frac{n}{2}2^{k-1}2^{k}两个整数之间取绝对值差值最小的,若n=7,\frac{n}{2}为4;n=11,\frac{n}{2}为4)的左偏树进行合并1次,所以总的复杂度为

\frac{n}{2}\times O(1)+\frac{n}{4}\times O(2)+\frac{n}{8}\times O(4)+...\frac{n}{2^{i}}\times O(2^{i-1})+...1\times O(\frac{n}{2})=O(\sum_{i=1}^{k}\frac{n}{2^{i}}\times 2^{i-1})=O(n)

2.1.4.删除最小值操作

删除最小值操作,即从二项堆中每个二项树的根节点中找到最小节点,然后删除最小节点,被删除根节点的二项树与其余的二项树执行“合并”过程,其位代码如下:

def extractMin(C)
{
find min in the root list
add min->left to the root List;
add min->right to the root List;
del min;
consolidation(C)
}

在根结点链表中,查找最小值最坏情况下为二相堆的长度,而长度不超过 \left \lfloor log(n) \right \rfloor+1,所以删除最小值的复杂度为O(log(n))

2.1.5 删除任意值操作

删除任意值指的是删除二项堆上已存在的值。删除任意值的步骤如下,首先找到这个任意值,设这个任意值所在的二项树称为T ,将此“任意值”和所在二项树T的根结点进行交换(即将要删除的节点“向上调整”至根节点),交换的过程要保持堆性质,交换完毕后,将新树T的左右子树改至根结点链表上,然后将更换完毕后新二项的根节点删除,剩下的部分执行“合并”,合并完后算法结束。删除任意值的操作过程 下。

删除任意值操作的伪代码如下:

def delany(T,x)
{
while(x.parent!=NULL)
{
swap(x,x.parent)
x=x.parent
}
del x
 return consolidation(x.child,others)
}

删除任意值的时间复杂度如下。向上调整的复杂度不超过O(log(n)), 合并其左右子树和剩余二项树的复杂度不超过O(log(n)),所以删除任意值的操作复杂度为O(log(n))。删除任意值操作还可以划分为两种操作的集合,请参考复习题。

2.1.6 减小值的操作

减小值的操作类似于堆的操作,减小后要进行“向上调整”或“向下调整”以满足堆性质。其复杂度向上调整的复杂度不超过O(log(n))

3总结

二项堆是一种可高效合并的堆,其它操作均可以建立在“合并”操作的基础上。其常见操作的复杂度见下表。

二项堆操作合并插入构建删除最小值删除任意值减小值
复杂度O(log(n)+log(m))O(log(n))O(n)O(log(n))O(log(n))O(log(n))

 

 

 

4 复习题

(1)请解释二项堆和二项树的区别和联系

(2)在2.1.5中说到删除任意值的操作可以分为两个操作的集合,哪两个?(答:减小值和删除最小值。先将要删除的值减小为-\infty,然后执行删除最小值操作)

参考资料:

1 Handbook of Data Structures and Applications.Dinesh P.Mehta and Sartaj Sahni.2005

 

 

 

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
二项Binomial Heap)是一种非常高效的数据结构,它可以在插入、删除和合并等操作上达到O(logn)的时间复杂度。下面是二项的C语言实现代码: ```c #include <stdio.h> #include <stdlib.h> // 二项节点结构体 typedef struct node { int val; // 节点的值 int degree; // 节点的度数 struct node *child; // 节点的左孩子 struct node *sibling; // 节点的右兄弟 } Node; // 创建节点 Node *create_node(int val) { Node *node = (Node *)malloc(sizeof(Node)); node->val = val; node->degree = 0; node->child = NULL; node->sibling = NULL; return node; } // 合并两个二项 Node *merge(Node *h1, Node *h2) { if (h1 == NULL) { return h2; } if (h2 == NULL) { return h1; } Node *head = NULL; // 合并后的的头节点 Node *tail = NULL; // 合并后的的尾节点 Node *p1 = h1; Node *p2 = h2; while (p1 != NULL && p2 != NULL) { if (p1->degree <= p2->degree) { if (head == NULL) { head = p1; } else { tail->sibling = p1; } tail = p1; p1 = p1->sibling; } else { if (head == NULL) { head = p2; } else { tail->sibling = p2; } tail = p2; p2 = p2->sibling; } } if (p1 != NULL) { tail->sibling = p1; } else { tail->sibling = p2; } return head; } // 插入节点 Node *insert(Node *h, int val) { Node *node = create_node(val); return merge(h, node); } // 查找最小值节点 Node *find_min(Node *h) { Node *min_node = h; Node *p = h->sibling; while (p != NULL) { if (p->val < min_node->val) { min_node = p; } p = p->sibling; } return min_node; } // 删除最小值节点 Node *delete_min(Node *h) { Node *min_node = find_min(h); if (min_node == h) { // 头节点就是最小值节点 h = h->sibling; } else { // 删除最小值节点,然后将其子树与其他树合并 Node *pre_min_node = h; while (pre_min_node->sibling != min_node) { pre_min_node = pre_min_node->sibling; } pre_min_node->sibling = min_node->sibling; Node *child_h = min_node->child; // 将最小值节点的子树反转后插入到中 Node *new_h = NULL; while (child_h != NULL) { Node *next_child_h = child_h->sibling; child_h->sibling = new_h; new_h = child_h; child_h = next_child_h; } h = merge(h, new_h); free(min_node); } return h; } int main() { Node *h = NULL; // 二项 int n, op, val; scanf("%d", &n); while (n--) { scanf("%d", &op); switch (op) { case 0: // 插入节点 scanf("%d", &val); h = insert(h, val); break; case 1: // 查找最小值节点 printf("%d\n", find_min(h)->val); break; case 2: // 删除最小值节点 h = delete_min(h); break; } } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值