在堆的实际应用中,我们经常要做一些堆的合并操作。
这时候,我们就需要一种新的数据结构:可并堆。
可并堆有很多种,例如:斜堆,左偏树,(配对堆、斐波那契堆,这两个不会说的,因为我不会)。
这里介绍两种斜堆和左偏树。
斜堆:
我们将根节点值较大的堆称作a,较小的堆称作b(小根堆相反)。
那么我们把a的右儿子和b合并作为新堆的右儿子,将a的左儿子作为新堆的左儿子。
那么我们会遇到这样一个情况,就是随着合并的次数增多,右儿子会变得比较大,而左儿子会变得很小。
那么我们可以想到一个显而易见的做法,那就是在每次合并后,交换左右儿子,这样下一次合并的时候就会合并到较小的子树上。
得到了这样的一个算法后,我们来考虑一下复杂度。
在不加优化的合并方式中,会出现一种情况就是几乎连成一条链,在这种形态下的单次合并复杂度会几乎达到O(N)。
而我们每次将左右儿子交换,就能保证不会出现这种情况。
因为事实上每一次新加入的元素是类似平均地加入到这棵树中(因为每次都是加到较小的子树中)。
这样就能保证合并的复杂度均摊到O(logn)了。
void merge(int rta,int rtb){
if(size[rta]==0)return rtb;
if(size[rtb]==0)return rta;
if(key[rta]<key[rtb])swap(rta,rtb);
rchild[rta]=merge(rchild[rta],rtb);
swap(lchild[rta],rchild[rta]);
return rta;
}
左偏树:
顺着斜堆的想法,我们可以想到,一次合并的效率与这次合并多快能遇到【一个只有一个儿子的节点】有关(因为在递归合并两棵子树时,若一棵为空,则合并完成)。
那么我们顺着这个思路,可以定义一个值为与点i最近的【一个只有一个儿子的节点】的距离为dis(i),并且规定每个点左儿子的dis一定比右儿子的dis大,否则交换左右儿子。
这样每个点的dis就是右儿子dis+1,dis(一个只有一个儿子的节点)=0。
合并时,与斜堆类似,递归地合并。将根节点值较小的树插到较大的树的右子树里(小根堆相反),在此过程中更新每个点的dis值,并通过交换左右儿子使它满足刚才的性质。
void merge(int rta,int rtb){
if(size[rta]==0)return rtb;
if(size[rtb]==0)return rta;
if(key[rta]<key[rtb])swap(rta,rtb);
rchild[rta]=merge(rchild[rta],rtb);
if(dis[lchild[rta]]<dis[rchild[rta]])
swap(lchild[rta],rchild[rta]);
dis[rta]=dis[rchild[rta]]+1;
return rta;
}
【两种可并堆的除合并以外的操作与堆相同,不再赘述】