介绍
堆作为一种相对易懂,易学,并且相对独立的数据结构(才会在STL中出现),广受大家使用,它的时间复杂度也很优秀,是排序算法中的佼佼者,仅需。这次讲的堆,作为进阶,自然更难一些,当然用处也更大。这种堆得主要用处在于可以将其合并起来,并且合并的速度远优于一个一个拿出来的速度。这种堆统称为可并堆(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)每项操作在最坏情况下都仅需的时间。网上查找300行左右。Fredman和Tarjan提出了斐波那契堆(Fibonacci heap),这能让除delete-min和delete(均摊下
)操作在
的时间下做完。当然,Brodal,Lagogiannis & Tarjan共同提出了Strict Fibonacci Heap(原谅我无法翻译),这种优秀的数据结构能使在原基础上使delete-min和delete操作的渐进时间复杂度的上界达到
(不再是均摊了,对于这个来说是最慢的情况)。斐波那契堆能优化Dijkstra算法(Dijkstra’s shortest path algorithm)(优化至
),最小树形图算法(朱刘算法或Edmonds’ minimum branching algorithm,两者应是等价的)和某些最小生成树算法。Fredman,Sedgewick,Sleator和Tarjan后来提出了一种和自适应操作相关的堆实现方法,配对堆(pairing heap)。这种结构仅对于delete-min和decrease-key操作均摊下需要
的时间,其它都是
的复杂度。当然,Haeupler,Sen和Tarjan又在后面提出了秩配对堆(rank-pairing heap)(大概如此翻译,指添加顺序)。这种也极为优秀,仅delete-min和delete需要均摊下
的时间,理论上略次于Strict Fibonacci Heap。但是,经过实践验证(实践是检验真理的唯一标准),Pairing Heap在实际操作中都更具优势(快)(大神和Tarjan都如此说)。当然,还有更加优秀的算法,但极为复杂,就不赘述了。这些优秀的算法往往被人们称为黑科技。我们再来看几种更为亲民的,待会儿讲的重点也会在它们身上。(鉴于上文某些算法不算不实用程度,且中文介绍几乎找不到,如果我看懂了并且未自尽,我会花篇幅来讲讲,当然,有可能放在拓展篇里。)
Sleator和Tarjan提出了斜堆(skew heap,又称自适应堆self-adjusting heap),它基于普通的二叉堆,均摊下每个操作的时间复杂度都为(准确来说是
,
约等于
)。Gambin和Malinowski提出了随机堆(randomized meldable heap),它与斜堆类似,只不过用了随机的玄学操作,每种操作的渐进时间复杂度(或者最坏情况下)都为
。Crane发明了左偏树(leftist heap,我觉得左偏堆更准确),它的特色在于保持左边的结点更重(改成右边是等价的)。它的delete-min,insert,merge操作渐进时间复杂度都为
,并且decrease-key操作达到了惊人的
(未免令人汗颜)。原生的二叉堆delete-min,decrease-key最坏
,insert为
,merge为
并且不支持delete操作。但是有一种启发式合并操作,可以达到
。
算法
鉴于我没有意向写初步版本的堆,我在这重述一下基础的二叉堆。
本篇文章以小根堆为例,大根堆自行更改符号、名称。堆保证子节点大于或等于父节点,二叉堆保证堆是一棵(近似的)完全二叉树。插入一个点时将这个点放在末尾,执行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;
}