题目
【问题描述】
编写一程序采用Huffman编码对一个正文文件进行压缩。具体压缩方法如下:
-
对正文文件中字符(换行字符’\n’除外,不统计)按出现次数(即频率)进行统计
-
依据字符频率生成相应的Huffman树(未出现的字符不生成)
-
依据Huffman树生成相应字符的Huffman编码
-
依据字符Huffman编码压缩文件(即按照Huffman编码依次输出源文件字符)。
说明:
-
只对文件中出现的字符生成Huffman,注意:一定不要处理\n,即不要为其生成Huffman码。
-
采用ASCII码值为0的字符作为压缩文件的结束符(即可将其出现次数设为1来参与编码).
-
在生成Huffman树时,初始在对字符频率权重进行(由小至大)排序时,频率相同的字符ASCII编码值小的在前;新生成的权重节点插入到有序权重序列中时,出现相同权重时,插入到其后(采用稳定排序)。
-
遍历Huffman树生成字符Huffman码时,左边为0右边为1。
-
源文件是文本文件,字符采用ASCII编码,每个字符点8位;而采用Huffman编码后,高频字符编码长度较短(小于8位),因此最后输出时需要使用C语言中的位运算将字符Huffman码依次输出到每个字节中。
【输入形式】
对当前目录下文件input.txt进行压缩。
【输出形式】
将压缩后结果输出到文件output.txt中,同时将压缩结果用十六进制形式(printf("%x",…))输出到屏幕上,以便检查和查看结果。
【样例输入1】
若当前目录下input.txt中内容如下:
aaabbc
【样例输出1】
15f0
同时程序将压缩结果输出到文件output.txt中。
【样例说明】
输入文件中字符的频率为:a为3,b为2,c为1,此外,\0字符将作为压缩文件的结束标志,其出现次数设为1。因此,采用Huffman码生成方法,它们的Huffman编码分别为:
a : 0
b : 10
c : 111
\0 : 110
因此,最终文件压缩结果(按位)为:
0001010111110000
将上述结果按字节按十六进制输出到屏幕上则为15f0(即0001010 111110000的十六进制表示)。
说明:采用Huffman码输出字符序列长度为:1+1+1+2+2+3+3=13(位),由于C语言中输出的最小单位为字节(8位),因此,最后补了三个位0,压缩后实际输出为2个字节。由于文本文件是按ASCII来解释的,因此,以文本方式打开压缩文件将显示乱码(最好用二进制文件查看器来看)。
【样例输入2】
若当前目录下input.txt中内容如下:
do not spend all that you have.do not sleep as long as you want.
【样例输出2】
ea3169146ce9eee6cff4b2a93fe1a5d462d21d9a87c0eb2f3eb2a9cfe6cae
同时程序将压缩结果输出到文件output.txt中。
我的一些感悟
这一道题耗费我的时间还是比较多的,总结了以下几点原因:
- 对于Huffman编码遗忘。平时应用较少,导致我对于Huffman编码有些陌生,不仅在于Huffman编码的构建过程,还在于Huffman树的建立,因此还是需要一些参考文献来进行复习。当时借鉴的参考文献如下
(所以古人说“学而时习之,不亦说乎”还是很有道理的):
- 在考虑容纳节点的数据结构时疏忽了有序性。第一次我写的时候使用了STL的set,后面进行排序的时候发现没法搞了,所以就换成了vector;
- 最后一个就是情况考虑不周全。在写的时候,总是有些莫名其妙的错误。最恶心人的一次就是用我自己写的程序和网上找的代码分别对一组测试数据进行测试,运行结果完全相同,但是提交到测评平台台上就是不给我算过,搞得我怀疑人生。最后发现是我考虑欠缺,唉。。
设计思路
对于这个题目,设计思路还是比较清楚的。首先是对文件中出现的字符进行统计①,然后根据统计出来的字符数目创建Huffman树②,生成对应的Huffman编码③,最后根据Huffman编码对文件进行编码④即可。
对于①,我使用了一个int类型的一维数组,大小为130。每读取到一个字符的时候,如果是换行符就continue(题目要求),否则以该字符为偏移量对数组指针偏移后的位置进行自增(相当于char型向int进行隐式转换),直到EOF。当然在最后的时候不要忘了对’\0’进行特殊处理。代码如下:
void read(int a[])
{
FILE *fp = fopen("./input.txt", "r");
if (fp == nullptr)
exit(-1);
while (!feof(fp))
{
char c = fgetc(fp);
if (c == '\n')
continue;
a[c]++;
}
a[0] = 1;
fclose(fp);
}
对于②,首先定义一种Node类型的数据结构:
typedef struct NODE
{
int val; //频率
char c; //字符
NODE *left, *right;
} Node;
表示Huffman树中的节点。开始的时候,先将所有的字符初始化成叶子节点并存放入一个vector<Node *> nodes的队列中,便于后续的取出和插入。然后对nodes进行排序,根据题目要求重写比较函数。然后进入循环,当nodes不为空的时候(后面解释),创建新节点tmp,取出队列前两个元素(节点)并将val求和赋值给tmp->val,同时将这两个节点赋值给tmp的左右子节点;然后根据题目要求,查询合适的位置,将tmp插入到那个位置。由于对于队列是一次取出两个元素并插入一个元素,所以总的来说是减少了一个,当队列元素减少到只剩下两个元素的时候,一次性取出来之后队列就空了,并且两个元素生成的tmp节点就是Huffman树的根节点。代码如下:
NODE *build_hfm(int data[])
{
vector<Node *> nodes;
Node *tmp = nullptr;
for (int i = 0; i < 130; i++)
if (data[i] > 0)
{
Node *tmp = (Node *)malloc(sizeof(Node));
tmp->val = data[i];
tmp->c = i;
tmp->left = tmp->right = nullptr;
nodes.push_back(tmp);
}
stable_sort(nodes.begin(), nodes.end(), [](Node *a, Node *b) {//使用lambda表达式
if (a->val != b->val)
return a->val < b->val;
return a->c < b->c;
}); //稳定排序
while (!nodes.empty())
{
tmp = (Node *)malloc(sizeof(Node));
tmp->val = nodes[0]->val + nodes[1]->val;
tmp->c = 0;
tmp->left = nodes[0];
tmp->right = nodes[1];
nodes.erase(nodes.begin());
nodes.erase(nodes.begin());
if (tmp->val < (*nodes.begin())->val)
{ //开始忘了加这个搞了好长时间
nodes.insert(nodes.begin(), tmp);
continue;
}
for (vector<Node *>::iterator it = nodes.begin(); it != nodes.end(); it++)
if (((*it)->val <= tmp->val && (*(it + 1))->val > tmp->val) || (it + 1 == nodes.end()))
{
nodes.insert(it + 1, tmp);
break;
}
}
return tmp;
}
对于③,我采用的是DFS,将一个string作为参数进行递归,并根据左右子树进行连接"0"或"1"的操作。当搜索到叶子节点时,该string即是对应的Huffman编码,保存到map<char, string>中即可。代码如下:
void dfs(Node *head, map<char, string> &res, string code)
{
if (head->left == nullptr && head->right == nullptr) //此节点为叶子节点
{
res.insert(make_pair(head->c, code));
return;
}
dfs(head->left, res, code + "0");
dfs(head->right, res, code + "1");
}
对于④,我首先将文件中的字符全部翻译成Huffman编码保存到一个string中,然后在其长度范围内进行自增为8(题目要求)的for循环操作。当循环变量移动到靠近结尾的时候可能出现不够8位的情况,这时候需要特殊处理进行补零。还有一点就是需要对表示二进制数字符串的转换,这样才能使用printf("%x",…)输出16进制。代码如下:
void print(map<char, string> m)
{
FILE *fpin = fopen("./input.txt", "r");
FILE *fpout = fopen("./output.txt", "w");
string buf;
char c;
if (fpin == nullptr || fpout == nullptr)
exit(-1);
while (!feof(fpin))
{
c = fgetc(fpin);
if (c == '\n')
continue;
buf += m[c];
}
buf += m[0];
unsigned len = buf.length();
for (unsigned i = 0; i < len; i += 8)
{
if (i + 8 < len)
{
int sum = 0;
for (unsigned j = i, k = 7; j < i + 8; j++, k--)
{
fputc(buf[j], fpout);
sum += (1 << k) * (buf[j] - '0');
}
printf("%x", sum);
}
else
{
int sum = 0;
for (unsigned j = i, k = 7; j < len; j++, k--)
{
fputc(buf[j], fpout);
sum += (1 << k) * (buf[j] - '0');
}
printf("%x", sum);
for (unsigned j = len; j < i + 8; j++)
fputc('0', fpout);
}
}
fclose(fpin);
fclose(fpout);
}
值得注意的是我在这里并没有严格按照题目要求上说的写成二进制文件(以文本文件形式打开之后是乱码),所以感兴趣的读者可以参考这篇文章
完整代码
#include <bits/stdc++.h>
using namespace std;
typedef struct NODE
{
int val;
char c;
NODE *left, *right;
} Node;
void read(int a[])
{
FILE *fp = fopen("./input.txt", "r");
if (fp == nullptr)
exit(-1);
while (!feof(fp))
{
char c = fgetc(fp);
if (c == '\n')
continue;
a[c]++;
}
a[0] = 1;
fclose(fp);
}
NODE *build_hfm(int data[])
{
vector<Node *> nodes;
Node *tmp = nullptr;
for (int i = 0; i < 130; i++)
if (data[i] > 0)
{
Node *tmp = (Node *)malloc(sizeof(Node));
tmp->val = data[i];
tmp->c = i;
tmp->left = tmp->right = nullptr;
nodes.push_back(tmp);
}
stable_sort(nodes.begin(), nodes.end(), [](Node *a, Node *b) {
if (a->val != b->val)
return a->val < b->val;
return a->c < b->c;
}); //稳定排序
while (!nodes.empty())
{
tmp = (Node *)malloc(sizeof(Node));
tmp->val = nodes[0]->val + nodes[1]->val;
tmp->c = 0;
tmp->left = nodes[0];
tmp->right = nodes[1];
nodes.erase(nodes.begin());
nodes.erase(nodes.begin());
if (tmp->val < (*nodes.begin())->val)
{
nodes.insert(nodes.begin(), tmp);
continue;
} //mlgb
for (vector<Node *>::iterator it = nodes.begin(); it != nodes.end(); it++)
if (((*it)->val <= tmp->val && (*(it + 1))->val > tmp->val) || (it + 1 == nodes.end()))
{ //队尾条件
nodes.insert(it + 1, tmp);
break;
}
}
return tmp;
}
void del(Node *head)
{
if (head->left != nullptr)
del(head->left);
if (head->right != nullptr)
del(head->right);
head->val = head->c = 0;
head->left = head->right = nullptr;
free(head);
}
void dfs(Node *head, map<char, string> &res, string code)
{
if (head->left == nullptr && head->right == nullptr) //此节点为叶子节点
{
res.insert(make_pair(head->c, code));
return;
}
dfs(head->left, res, code + "0");
dfs(head->right, res, code + "1");
}
void print(map<char, string> m)
{
FILE *fpin = fopen("./input.txt", "r");
FILE *fpout = fopen("./output.txt", "w");
string buf;
char c;
if (fpin == nullptr || fpout == nullptr)
exit(-1);
while (!feof(fpin))
{
c = fgetc(fpin);
if (c == '\n')
continue;
buf += m[c];
}
buf += m[0];
unsigned len = buf.length();
for (unsigned i = 0; i < len; i += 8)
{
if (i + 8 < len)
{
int sum = 0;
for (unsigned j = i, k = 7; j < i + 8; j++, k--)
{
fputc(buf[j], fpout);
sum += (1 << k) * (buf[j] - '0');
}
printf("%x", sum);
}
else
{
int sum = 0;
for (unsigned j = i, k = 7; j < len; j++, k--)
{
fputc(buf[j], fpout);
sum += (1 << k) * (buf[j] - '0');
}
printf("%x", sum);
for (unsigned j = len; j < i + 8; j++)
fputc('0', fpout);
}
}
fclose(fpin);
fclose(fpout);
}
int main(int argc, char *argv[])
{
int data[130] = {0};
Node *head = nullptr;
map<char, string> code;
read(data);
head = build_hfm(data);
dfs(head, code, "");
print(code);
del(head);
head = nullptr;
return 0;
}