有n个数(即n个叶子节点),构造k叉(k>=2)哈夫曼树的方法;
构造哈夫曼树,其实就是不停的“合并”的过程。并且每次合并,我们都是取前k个最小的数。算法的主要复杂就在于如何取前k个最小的数;不停排序或者优先队列、堆都可以做到,但复杂都接nlogn,那么我们可不可以只排序一次就能每次都取出前k个数呢?
当然可以
我们可以维护两个数组利用类似于“归并排序”的想法,O(n)的构造出k叉哈夫曼树。
第一步:先将n个数从小到大排序一次,放入第一个数组a;取k个数合并成一个新的数放入数组b的末尾。
之后,每次从两个数组里挑选出k个最小的数合并再次放到数组b的末尾。
这里不难发现:b数组是有序的。因为每次放入的都是最小的k个数之和,第二次放的肯定比第一次大。
那么也就是a,b数组都是有序的,所以之前说的从两个数组里取k个最小的数出来也就不难做到,只要维护两个指针,都指向数组的第一个位置,然后每次比较谁小就取谁,被取的那个指针往后移一位。
最后,当a数组为空,b数组只剩一个数时算法结束。(这个数即哈夫曼的根)
在以上的描述中,为了方便我省略了一个细节:
注意到,每次我们都是取k个数,但在最后一次取的时候,可能已经不足k个数可取了,对于这里,有两种处理方法,第一种是补充哈夫曼树也可以是k叉的,只是在构造k叉哈夫曼树时需要先进行一些调整。解决这个问题的办法是假设已经有了一棵哈夫曼树(且为一棵满k叉树),则可以计算出其叶节点数目为(k-1)nk+1,式子中的nk表示子节点数目为k的节点数目(也就是上面的中间结点以及根节点)。
于是对给定的n个权值构造k叉哈夫曼树时,可以先考虑增加一些权值为0的叶子节点,使得叶子节点总数为(k-1)nk+1这种形式,然后再按照哈夫曼树的方法进行构造即可。
第二种不妨在算法的开始,我们就先取掉x个数,使得剩下的n-x个数正好能每次取k个取完,也就是利用第一种推导出的公式进行删除
if((N-k)%(k-1)==0) num=k;
else num=(N-k)%(k-1)+1;
其中N为叶子结点的数量
算法总的来说,就是两个指针从首扫到尾,时间复杂度不超过O(2*n),已经是非常优秀。
关于k叉哈夫曼树在ACM中的最常用的应用无疑就是求合并n个数最小的代价。
关于K叉哈夫曼树的模板如下:
int hafuman(int k)
{
int ai,bi,blen;
blen=0;
ai=bi=0;
int cost=0;
bool first=true;
while(N-ai+blen-bi>1){
int num=0;
if(first){
if((N-k)%(k-1)==0){
num=k;
}else{
num=(N-k)%(k-1)+1;
}
first=false;
}else{
num=k;
}
int sum=0;
while(num--){
if(ai==N){
sum+=b[bi];
bi++;
}else if(bi==blen){
sum+=a[ai];
ai++;
}else if(a[ai]<b[bi]){
sum+=a[ai];
ai++;
}else{
sum+=b[bi];
bi++;
}
}
cost+=sum;
b[blen++]=sum;
}
return cost;
}