算法作业三-哈夫曼编码

狂风中的少年 提交于 2020-02-08 05:51:51

实验三 哈夫曼编码

问题描述与实验目的:

给定n个字母(或字)在文档中出现的频率序列X=<x1,x2,…,xn>,求出这n个字母的Huffman编码。为方便起见,以下将频率用字母出现的次数(或称权值)w1,w2,…,wn代替。

输入

输入文件中的开始行上有一个整数T,(0<T<=20),表示有T组测试数据。

接下来是T行测试数据的描述,每组测试数据有2行。测试数据的第1行上是一个正整数n,(n<50),表示序列的长度。第2行是n个字母出现的权值序列w1,w2,…,wn,它们均为正整数,相邻的两个整数之间用空格隔开。

输入直到文件结束。

输出

对输入中的每组有n个权值的数据,应输出n+1行:先在一行上输出“Case #”,其中“#”是测试数据的组号(从1开始);接下来输出n行,其第1行到第n行上依次输出第i个字母出现的次数和相应的Huffman编码,格式如下:

wi Huffman编码。

每组测试数据对应的输出最后结束时加一个空行,以便区分。

为保证Huffman编码的唯一性,在构造Huffman树的过程中,我们约定:

1.左儿子标记为0,右儿子标记为1;

2.左儿子的权值>=右儿子的权值;

3.相同权值w的两个字母x、y,先输入权值的字母x的Huffman编码长度不超过后输入权值的字母y的Huffman编码长度。

4.合并两个节点后新的权值应从右到左搜索、插入到相应的位置。

例如:输入权值序列8 9 3 4 1 2,其Huffman编码求解过程如下,参考图A-J:

注意,如图C中权值1、2对应的节点合并后得权值为3的新节点,它插入到权值为3的原有节点的右边。

输入样例

2
6
9 8 3 4 1 2
8
60 20 5 5 3 3 3 1

输出

Case 1
9 00
8 01
3 100
4 11
1 1011
2 1010

Case 2
60 0
20 10
5 1101
5 1110
3 11000
3 11001
3 11110
1 11111

分析:

哈夫曼编码本质是个贪心的算法。将每个字符视作一个带权结点(子树)。
每次优先选择权值最小的两个子树,将二者合并,权值相加,权值小的作为右子结点(1),权值大的作左子结点(0),形成一个新的子树。再将这棵子树放回优先队列中,重复以上的操作。

本题要输出每个字符对应的哈夫曼编码,数据n<50较小,所以不考虑用数据结构建树,用直接保存路径的方法实现。自定义一个结构体,内部记录子树的权值和所有叶子结点。
用优先队列存储所有子树,因为堆排序是一种不稳定排序,可能导致相同权值的子树顺序颠倒,所以重载比较运算符,使用两个关键字排序。
两个子树合并的时候,将左子树记录的叶子结点编码全部加上''0',右子树记录的结点编码全部加'1'。当队列中结点数为1时,算法结束。
最后根据读入的顺序输出0-1字符串即可。

运行结果:

Alt text

OJ测评结果:

Alt text

源代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 105;

string res[55];

struct node{
    int val,id;
    vector<int> dp;
    bool operator < (const node & rhs) const{       //双关键字排序
        if(val==rhs.val) return id < rhs.id;
        return val > rhs.val;
    }
}vz[maxn];

int main()
{
    #ifndef ONLINE_JUDGE
        freopen("in.txt","r",stdin);
        freopen("out.txt","w",stdout);
    #endif
    int T,cas=1; scanf("%d",&T);
    while(T--){
        int n;
        scanf("%d",&n);
        for(int i=0;i<=n;++i) res[i].clear();

        priority_queue<node> Q;
        for(int i=1,w;i<=n;++i){
            node tmp;
            scanf("%d",&w);
            vz[i].val = w;
            tmp.val = w;
            tmp.id = i;
            tmp.dp.push_back(i);
            Q.push(tmp);             //初始将每个结点视作子树
        }
        int cnt = n+1;
        while(!Q.empty()){
            node x = Q.top(); Q.pop();
            if(Q.empty()) break;
            node y = Q.top(); Q.pop();      //取出权值最小的两个子树
            node t;
            t.val = x.val + y.val;          //合并权值
            t.id = cnt++;
            for(int i=0,sz = x.dp.size() ;i < sz ;++i){          //左子树
                int id = x.dp[i];
                res[id].push_back('1');
                t.dp.push_back(id);         //合并叶子结点
            }
            for(int i=0,sz = y.dp.size();i < sz; ++i){          //右子树
                int id = y.dp[i];
                res[id].push_back('0');
                t.dp.push_back(id);
            }
            Q.push(t);
        }
        printf("Case %d\n",cas++);
        for(int i=1;i<=n;++i){
            reverse(res[i].begin(),res[i].end());
            cout<<vz[i].val<<" "<<res[i]<<endl;
        }
        puts("");
    }
    return 0;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!