堆进阶-Meldable Heap

本文介绍了可并堆(meldable heap)的概念及其重要性,包括二项队列、斐波那契堆、严格斐波那契堆、配对堆、秩配对堆等数据结构。特别强调了在实际应用中,配对堆由于其优秀性能而更受欢迎。此外,文章还探讨了斜堆、随机堆和左偏树等相对亲民的堆实现,它们在保持高效的同时,简化了操作复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

介绍

堆作为一种相对易懂,易学,并且相对独立的数据结构(才会在STL中出现),广受大家使用,它的时间复杂度也很优秀,是排序算法中的佼佼者,仅需O\left ( nlog_{n} \right )。这次讲的堆,作为进阶,自然更难一些,当然用处也更大。这种堆得主要用处在于可以将其合并起来,并且合并的速度远优于一个一个拿出来的速度。这种堆统称为可并堆(meldable heap),合并(meld)操作和删除(delete)任意结点操作是它的特色。还是给出一个简单的概括:

可并堆(后文简称堆)是一种包含了一组结点的数据结构。每个结点都有一个不同的实际值。堆支持以下操作:

-makeheap:返回一个空的新的堆。

-insert(x,H):把一个有值的在堆外的结点x插入堆H。

-find-min(H):返回H中的最小值。

-delete-min(H):如果H不为空,删除其中最小的结点。

-meld(H1,H2):返回一个堆,其中包含不相交的两个堆H1,H2中的所有结点,并删除H1,H2。

某些堆的应用需要以下操作中的至少一种:

-decrease-key(x,Δ,H):将H中结点x的值减少Δ>0。 

-delete(x,H):将结点x从H中删除。

(此部分从Haeupler, Sen & Tarjan论文翻译得)

这种堆的数量很多,我们提一些真正能够被用到的数据结构(在工作和竞赛中,不要说什么码完考试都完了)。Vuillemin的二项队列(biominal queue,又称二项堆biominal heap)每项操作在最坏情况下都仅需O(logn)的时间。网上查找300行左右。Fredman和Tarjan提出了斐波那契堆(Fibonacci heap),这能让除delete-min和delete(均摊下O(logn))操作在O(1)的时间下做完。当然,Brodal,Lagogiannis & Tarjan共同提出了Strict Fibonacci Heap(原谅我无法翻译),这种优秀的数据结构能使在原基础上使delete-min和delete操作的渐进时间复杂度的上界达到O(logn)(不再是均摊了,对于这个来说是最慢的情况)。斐波那契堆能优化Dijkstra算法(Dijkstra’s shortest path algorithm)(优化至O\left ( V+ElogE \right )),最小树形图算法(朱刘算法或Edmonds’ minimum branching algorithm,两者应是等价的)和某些最小生成树算法。Fredman,Sedgewick,Sleator和Tarjan后来提出了一种和自适应操作相关的堆实现方法,配对堆(pairing heap)。这种结构仅对于delete-min和decrease-key操作均摊下需要O(logn)的时间,其它都是O(1)的复杂度。当然,Haeupler,Sen和Tarjan又在后面提出了秩配对堆(rank-pairing heap)(大概如此翻译,指添加顺序)。这种也极为优秀,仅delete-min和delete需要均摊下O(logn)的时间,理论上略次于Strict Fibonacci Heap。但是,经过实践验证(实践是检验真理的唯一标准),Pairing Heap在实际操作中都更具优势(快)(大神和Tarjan都如此说)。当然,还有更加优秀的算法,但极为复杂,就不赘述了。这些优秀的算法往往被人们称为黑科技。我们再来看几种更为亲民的,待会儿讲的重点也会在它们身上。(鉴于上文某些算法不算不实用程度,且中文介绍几乎找不到,如果我看懂了并且未自尽,我会花篇幅来讲讲,当然,有可能放在拓展篇里。)

Sleator和Tarjan提出了斜堆(skew heap,又称自适应堆self-adjusting heap),它基于普通的二叉堆,均摊下每个操作的时间复杂度都为O(logn)(准确来说是O(log_{\phi }n)\phi=\frac{1+\sqrt{5}}{2}约等于O\left ( 1.44log_{2}n \right ))。Gambin和Malinowski提出了随机堆(randomized meldable heap),它与斜堆类似,只不过用了随机的玄学操作,每种操作的渐进时间复杂度(或者最坏情况下)都为O(logn)。Crane发明了左偏树(leftist heap,我觉得左偏堆更准确),它的特色在于保持左边的结点更重(改成右边是等价的)。它的delete-min,insert,merge操作渐进时间复杂度都为O(logn),并且decrease-key操作达到了惊人的O(n)(未免令人汗颜)。原生的二叉堆delete-min,decrease-key最坏O(logn),insert为O(logn),merge为O(n)并且不支持delete操作。但是有一种启发式合并操作,可以达到O\left ( nlog^{2}n \right )

算法

鉴于我没有意向写初步版本的堆,我在这重述一下基础的二叉堆。

本篇文章以小根堆为例,大根堆自行更改符号、名称。堆保证子节点大于或等于父节点,二叉堆保证堆是一棵(近似的)完全二叉树。插入一个点时将这个点放在末尾,执行pushup向上推;删除根时,将根置为最末一个节点,执行pushdown向下推。

对于合并复杂度较低的堆结构,它们就不再以完全二叉树为基础了。所以应使用链式结构储存数据。左偏树、斜堆、随机堆是类似的数据结构。对于它们的时间复杂度都可以证明,读者自证可得。左偏树需要记录每个结点dis值,即它到最近外结点的距离。保证左儿子的dis大于或等于右儿子的。合并时将根节点较小的设为x,另一个设为y。在x右子树中递归合并y,递归返回时,若不符合dis性质,交换左右子树。其它操作类似二叉堆。

斜堆类似但不同于左偏树,递归过后直接交换左右子树即可,不需记录dis。随机堆和斜堆几乎一样,只是把交换操作随机进行。

代码

//a: a vector which denotes the heap
void pushup(int i)
{
	int child=i;
	int father=i>>1;
	int temp=a[i]
	while(father>=0)
	{
		if(a[father]<=temp)break;
		else
		{
			a[child]=a[father];
			child=father;
			father=child>>1;
		}
	}
	a[child]=temp;
	return;
}
void pushdown(int root)
{
	int father=root;
	int child=root<<1;
	int temp=a[father];
	while(child<a.size())
	{
		if(a[child]>=temp)break;
		else
		{
			a[father]=a[child];
			father=child;
			child=father<<1;
		}
	}
	a[father]=temp;
	return;
}

Leftist Heap

*node merge(*node x,*node y)
{
	if(x==NULL)return y;
	if(y==NULL)return x;
	if(x->key>y->key)swap(x,y);
	x->right=merge(x->right,y);
	if(x->lc==NULL||(x->lc->dis<x->rc->dis))swap(x->lc,x->rc);
	return x;
}

Skew Heap

*node merge(*node x,*node y)
{
	if(x==NULL)return y;
	if(y==NULL)return x;
	if(x->key>y->key)swap(x,y);
	x->right=merge(x->right,y);
	swap(x->lc,x->rc);
	return x;
}

Randomized Meldable Heap

*node merge(*node x,*node y)
{
	if(x==NULL)return y;
	if(y==NULL)return x;
	if(x->key>y->key)swap(x,y);
	x->right=merge(x->right,y);
	if(rand()%2)swap(x->lc,x->rc);
	return x;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值