编写一个哈夫曼编码译码程序。针对一段文本,根据文本中字符出现频率构造哈夫曼树,给出每个字符的哈夫曼编码,并进行译码,计算编码前后文本大小。
为确保构建的哈夫曼树唯一,本题做如下限定:
- 选择根结点权值最小的两棵二叉树时,选取权值较小者作为左子树。
- 若多棵二叉树根结点权值相等,则先生成的作为左子树,后生成的作为右子树,具体来说:i) 对于单结点二叉树,优先选择根结点对应字母在文本中最先出现者,如文本为cba,三个字母均出现1次,但c在文本中最先出现,b第二出现,故则选择c作为左子树,b作为右子树。ii) 对于非单结点二叉树,先生成的二叉树作为左子树,后生成的二叉树作为右子树。iii. 若单结点和非单结点二叉树根结点权值相等,优先选择单结点二叉树。
- 生成哈夫曼编码时,哈夫曼树左分支标记为0,右分支标记为1。
输入格式:
输入为3行。第1行为一个字符串,包含不超过5000个字符,至少包含两个不同的字符,每个字符为a-z的小写字母。第2、3行为两个由0、1组成的字符串,表示待译码的哈夫曼编码。
输出格式:
输出第一行为用空格间隔的2个整数,分别为压缩前后文本大小,以字节为单位,一个字符占1字节,8个二进制位占1字节,若压缩后文本不足8位,则按1字节算。输出从第二行开始,每行为1个字符的哈夫曼编码,按各字符在文本中出现次数递增顺序输出,若多个字符出现次数相同,则按其在文本出现先后排列。每行格式为“字母:编码”。最后两行为两行字符串,表示译码结果,若译码失败,则输出INVALID。
输入样例:
cbaxyyzz
0100
011
输出样例:
8 3
c:100
b:101
a:110
x:111
y:00
z:01
zy
INVALID
解题思路:我们在构建哈夫曼树的时候,首先要算出给定字符串中各个字母出现的频率,因为这个题目中说字母的出现频率可能有相同的,那就先把出现位置最靠前的2个拿出来,对于每次选节点的顺序问题,建议反复阅读题目,这里就说一下大体的思路,本人认为这个题的难点在于如何去实现,至于思路读完题差不多就能想出来了。
首先给出一个字符串,我们根据这个字符串将每个字母出现的次数进行统计,并且根据字母第一次出现的位置为每个字母指定一个优先级,出现的越靠前优先级越小,因为考虑到每次选节点都要考虑2个因素的大小问题,我们可以想到用一个优先队列来为动态维护,排序的依据就是如果2个字符的出现次数不相同就按出现次数从小到大排序,如果出现次数相同,就按照根据出现位置指定的优先级从小到达来排序,而优先队列中的元素是我们将每个字符的信息包装成一个节点(包括这个节点代表的字符是什么,出现的次数,优先级,左孩子,右孩子)。
然后开始构建哈夫曼树,每次从优先队列中取出2个节点,将2个节点合并成一个节点,根据取出节点的信息来构造出合并节点的信息,我们指定新节点的字符是一个大写字母,后期我们也是通过节点代表的字母是大写的还是小写的来区别该节点是经过合并后形成的节点还是由给定字符串中的字符形成的节点,出现次数是2个节点的次数之和,优先级指定为当前最大的一个优先级数值,表示频率相同情况下,该节点的优先级最低,并将出队的2个节点作为新节点的左右孩子,最后将该节点加入队列,在我们构建哈夫曼树的时候可以顺便把频率加起来,最后的总频率就是最后译码后二进制代码的个数 。
接下来就是为各个字符编码的环节,我们从树的根节点(根节点就是我们构建哈夫曼树后优先队列中最后剩下的节点)出发,用深度优先遍历的方法,分别遍历左右孩子,并在遍历的过程中用一个vector来记录到达当前位置时的编码,通过判断当前节点的字符是否是小写字符,如果是小写字符那么这个节点就不是合成的节点,我们就可以把当前的编码和字符建立对应关系给存起来。
最后是译码阶段,对于给出的一个编码,我们也是从根节点出发,根据给定编码去遍历哈夫曼树,即如是‘0’就去遍历左子树,如果是‘1’就去遍历右子树,一旦遍历到非合成节点,将该节点对应的字符记录下来,回到根节点继续遍历,如果到最后一个字符时当前节点是合成的节点,那么这个编码就是错误的,输出INVALID即可,否则就将遍历过程中记录下来的字符按顺序输出即可。
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <map>
#include <queue>
#include <vector>
using namespace std;
struct node{
char c;//字符
int hz;//频率
int p;//优先级
node* left;
node* right;
};
struct cmp{//先按字符出现的频率从小到大排列,如果相同就按优先级从小到大排列
bool operator()(const node*a,const node*b){
if(a->hz!=b->hz)
return a->hz>b->hz;
else
return a->p>b->p;
}
};
string x,y,z;
int idx,sum;//idx用于设置优先级,sum记录原序列译码后的总长度
map<char,int>mp,np;//mp记录字符出现的频率,np记录字符的优先级
vector<char>ma;//用于译码
priority_queue<node*,vector<node*>,cmp>q,r;//q用于构造哈夫曼树,r用于按字符出现的顺序输出编码
map<char,vector<int> >cp;//记录每个字符的编码
vector<int>v;//遍历哈夫曼树生成每个字符的编码
void dfs(node* root)//为每个字符进行编码
{
if(root)
{
if(islower(root->c))
cp[root->c]=v;
v.push_back(0);
dfs(root->left);
v.pop_back();
v.push_back(1);
dfs(root->right);
v.pop_back();
}
}
void translation(string x)//译码函数
{
ma.clear();
node* t=new node;
t=q.top();
bool flag=true;
for(int i=0;i<x.size();i++)
{
if(x[i]=='0')
t=t->left;
else
t=t->right;
if((i==(x.size()-1))&&isupper(t->c))
flag=false;
if(islower(t->c))
{
ma.push_back(t->c);
t=q.top();
}
}
if(flag)
for(auto t:ma)
cout<<t;
else
cout<<"INVALID";
cout<<endl;
}
int main()
{
cin>>x>>y>>z;
for(int i=0;i<x.size();i++)//统计字符出现的频率和优先级
{
char temp=x[i];
mp[temp]++;
if(!np[temp])
np[temp]=idx++;
}
for(auto temp:mp)//包装节点
{
node* t=new node;
t->c=temp.first;
t->hz=temp.second;
t->p=np[temp.first];
t->left=t->right=NULL;
q.push(t);
r.push(t);
}
while(q.size()!=1)//构造哈夫曼树
{
node* a=new node;
a=q.top();
q.pop();
node* b=new node;
b=q.top();
q.pop();
node* t=new node;
t->c='S';
t->p=idx++;
t->hz=a->hz+b->hz;
t->left=a;
t->right=b;
q.push(t);
sum+=t->hz;
}
cout<<x.size()<<" ";
int cnt=sum/8;
if(cnt*8<sum)
cnt++;
cout<<cnt<<endl;
dfs(q.top());
while(r.size())
{
char t=r.top()->c;
cout<<t<<":";
for(auto temp:cp[t])
cout<<temp;
cout<<endl;
r.pop();
}
translation(y);
translation(z);
return 0;
}
这个题的实现感觉确实比较难,可能是我太菜了,还是借鉴了几个博客才搞出来。。。。菜哭了QAQ