[问题描述]
对一篇不少于5000字符的英文文章(Huffman编码材料.txt),统计各字符出现的次数,实现Huffman编码(code.dat),以及对编码结果的解码(recode.txt)。
[基本要求]
(1) 输出每个字符出现的次数和编码,并存储文件(Huffman.txt)。
(2) 在Huffman编码后,英文文章编码结果保存到文件中(code.dat),编码结果必须是二进制形式,即0 1的信息用比特位表示,不能用字符‘0’和‘1’表示(*)。
(3) 实现解码功能。
原文章:
对于这道题,首先应该求出每个字符出现的次数,作为哈夫曼编码的权重,这里我们只需打开文件,读取文件中的数据,再统计字符个数即可。之后我们开始建立哈夫曼树(这里我就不多说了,不知道如何建哈夫曼树的可以看我之前的文章,有详细解说),哈夫曼树建立完成后,我们即可求得每个字符的编码方式。
到这里,如果之前写过哈夫曼树的建立,做起来就比较容易,但我们也只完成了第一个问题,比较麻烦的是后面两问。第二问实质就是让我们对文章内容进行压缩,并保存到一个二进制文件中;第三问是让我们根据压缩得到的二进制文件还原文章。
压缩:现在,我们只是知道了每一个字符的编码方式,那么如何根据编码方式对文章内容进行压缩呢?这里我采用了一个比较暴力的方法。先将原来所有字符的哈夫曼编码连成一个长字符串(均为0、1),然后每8位取一组(注:一个字符是8个bit,所以每8位取一个),组成一个无符号类型的字符(unsigned char),这里要重点处理的一个问题是,最后不足8位的,我们要在后面补0来凑成8位,然后多读取一个提示符,提示最后一个凑成的字符原来的位数(因为在解码时,后面补的0是多余的,我们要根据提示符来把这些0舍去)。
解码:我们把字符依次从二进制文件中读出来,再把它们转回二进制的0、1形式,具体做法就是除2取余,最后不足8位的补0,再逆序,这样就可以还原原来的哈夫曼编码。当读到倒数第二个字符时(因为最后一个是我们多加的提示符,所以倒数第二个字符即为原来的最后一个字符),先把它转成8位0、1字符串,接着把提示符读出来,根据提示符的大小来截断8位字符串。到此我们就可以得到原来所有字符的哈夫曼编码连成的长字符串。再根据哈夫曼编码的方式对其进行解码即可。
注:编码和解码应该是分开进行的,解码的时候知道的只有编译后的二进制文件和解码方式。不应从之前编码的操作中直接获取提示(例如直接就知道最后一个8位字符串应该保留几位)。
源代码:
//这里的哈夫曼编码采用顺序结构
# include <iostream>
# include <fstream>
# include <algorithm>
# include <string>
# define SIZE 128
# define Infinity 1e9
using namespace std;
//字符型结构体
typedef struct Character {
char c; //字符
int weight; //权重
char code[20]; //编码结果
}Character, * Chara;
typedef struct HTNode {
int data; //对应的权重值
char c; //对应的字符(只有叶结点才有)
int parent, lchild, rchild;
}HTNode, * HufTree;
Chara ch; //保存所有字符信息
HufTree HT; //哈夫曼树
string str; //以01的形式保存所有的字符
int total = 0; //字符总数
int Visit[SIZE] = { 0 }; //统计都有哪些字符,并保存字符的数组下标
void CountChar(); //统计文件里的字符及权重
void Hufcode(); //哈夫曼编码
void Print(); //输出每个字符出现的次数和编码
void CodeFile(); //对文件内容进行哈夫曼编码
void DeCode(); //解码
int main()
{
CountChar();
Hufcode();
Print();
CodeFile();
DeCode();
return 0;
}
void CountChar() //统计文件里的字符及权重
{
fstream file;
file.open("Huffman编码材料.txt", ios::in);
if (file.fail()) {
cout << "Huffman编码材料.txt打开失败" << endl;
exit(0);
}
char c;
ch = (Chara)malloc(SIZE * sizeof(Character));
file.get(c); //一次读取单个字符
while (!file.eof()) {
int k = c;
//查看该字符是否已在数组中
if (Visit[k]) {
ch[Visit[k]].weight++;
}
else {
total++;
ch[total].c = c;
ch[total].weight = 1;
Visit[k] = total;
}
file.get(c);
}
file.close();
}
void Hufcode() //哈夫曼编码
{
HT = (HufTree)malloc(2 * total * sizeof(HTNode)); //哈夫曼树总结点有2*total-1个,动态分配2*total个空间
for (int i = 1; i <= total; i++) {
//建立原始结点
HT[i].data = ch[i].weight;
HT[i].c = ch[i].c;
HT[i].parent = -1;
HT[i].lchild = -1;
HT[i].rchild = -1;
}
//构建哈夫曼树
//哈夫曼树中共有2*total-1个结点,所以循环到2*total-1
for (int i = total + 1; i < 2 * total; i++) {
//寻找两个根节点权值最小的树
int m1 = i - 1, m2 = i - 1; //m1保存第一小的位置,m2保存第二小的位置
int x1 = Infinity, x2 = Infinity; //x1保存第一小的值,x2保存第二小的值
for (int j = 1; j < i; j++) {
//从第一个结点到当前的最后一个结点,寻找两个权重最小的位置
if (HT[j].parent == -1 && HT[j].data < x1) {
//符合条件的值,双亲必须为空
x2 = x1;
x1 = HT[j].data;
m2 = m1; //m2接替原m1,保存当前第二小的位置
m1 = j; //将当前最小值的位置赋给m1
}
else if (HT[j].parent == -1 && HT[j].data < x2) {
x2 = HT[j].data;
m2 = j;
}
}
//添加新树
HT[m1].parent = i; //添加双亲
HT[m2].parent = i; //添加双亲
HT[i].data = HT[m1].data + HT[m2].data; //新加入的结点,权重为两个最小值的和
HT[i].lchild = m1; //将第一小的位置作为左子树
HT[i].rchild = m2; //将第二小的位置作为右子树
HT[i].parent = -1; //新结点的双亲为空
}
//依据哈夫曼树,求各原始节点的编码
for (int i = 1; i <= total; i++) {
char s[20];
int j = i, k = 0;
int p = HT[j].parent;
while (p != -1) {
if (j == HT[p].lchild) {
s[k] = '0'; //如果是双亲的左子树则编为0
}
else {
s[k] = '1'; //如果是双亲的右子树则编为1
}
k++;
j = p;
p = HT[p].parent;
}
s[k] = '\0';
for (int l = 0; l < k; l++) {
//倒序输出的才是正确的编码方式
ch[i].code[k - 1 - l] = s[l];
}
ch[i].code[k] = '\0';
}
}
void Print() //输出每个字符出现的次数和编码
{
fstream file;
file.open("Huffman.txt", ios::out);
if (file.fail()) {
cout << "Huffman.txt打开失败" << endl;
exit(0);
}
file << "编号" << '\t' << "字符" << '\t' << "个数" << '\t' << "编码" << endl;
for (int i = 1; i <= total; i++) {
//将字符、字符出现的次数和编码转换成字符串保存到文本文件Huffman.txt中
file << i << '\t' << ch[i].c << '\t' << ch[i].weight << '\t' << ch[i].code << endl;
}
file.close();
cout << "每个字符出现的次数和编码:\n" << endl;
file.open("Huffman.txt", ios::in);
if (file.fail()) {
cout << "Huffman.txt打开失败" << endl;
exit(0);
}
char s[100];
file.getline(s, 100);
while (!file.eof()) {
cout << s << endl;
file.getline(s, 100);
}
file.close();
}
void CodeFile() //对文件内容进行哈夫曼编码
{
fstream file1, file2;
file1.open("Huffman编码材料.txt", ios::in);
file2.open("code.dat", ios::out | ios::binary);
if (file1.fail()) {
cout << "Huffman编码材料.txt文件打开失败" << endl;
exit(0);
}
if (file2.fail()) {
cout << "code.dat文件打开失败" << endl;
exit(0);
}
char c;
file1.get(c);
while (!file1.eof()) {
int k = c;
for (int i = 0; i < strlen(ch[Visit[k]].code); i++) {
str.append(1, ch[Visit[k]].code[i]);
}
file1.get(c);
}
string str1;
//采用无符号字符类型,范围为0-255(与8位对应)
unsigned char c1;
int a;
for (int i = 0; i < str.length(); i++) {
//每8位取一个,组成一个新字符(无符号)
if (i % 8 == 0 && i != 0) {
a = 0;
for (int j = 0; j < 8; j++) {
a = a + pow(2, 7 - j) * (str1[j] - 48);
}
c1 = a;
file2.write((char*)&c1, sizeof(c1));
str1.clear();
str1.append(1, str[i]);
}
else {
str1.append(1, str[i]);
}
}
a = 0;
for (int j = 0; j < str1.length(); j++) {
a = a + pow(2, 7 - j) * (str1[j] - 48);
}
c1 = a;
file2.write((char*)&c1, sizeof(c1));
c1 = str1.length() + 48; //多读一位,作为对前一位的位数的提示
file2.write((char*)&c1, sizeof(c1));
file1.close();
file2.close();
}
void DeCode() //解码
{
fstream file1, file2;
file1.open("code.dat", ios::in | ios::binary);
file2.open("recode.txt", ios::out);
if (file1.fail()) {
cout << "code.dat文件打开失败" << endl;
exit(0);
}
if (file2.fail()) {
cout << "recode.txt文件打开失败" << endl;
exit(0);
}
file1.seekg(-1L, ios::end);
int p = file1.tellg(); //获取倒数第二的位置
file1.seekg(0L, ios::beg);
string s, s1;
unsigned char c, c1;
int a, b = 0, d;
file1.read((char*)&c, sizeof(c));
while (!file1.eof()) {
if (file1.tellg() == p) {
//读到倒数第二个字符
b = 1;
}
a = c;
while (a > 0) {
c1 = a % 2 + 48;
s1.append(1, c1);
a = a / 2;
}
int len = s1.length();
for (int i = 0; i < 8 - len; i++) {
//如果转成的二进制数不足8位,则要在前面补0
s1.append(1, '0');
}
reverse(s1.begin(), s1.end());
if (b) {
//读取最后一个数位提示符,说明保留几位
file1.read((char*)&c, sizeof(c));
d = c - 48;
s1.assign(s1, 0, d);
}
s.append(s1);
s1.clear();
if (b) {
break;
}
else {
file1.read((char*)&c, sizeof(c));
}
}
int k = 0;
while (k < s.length()) {
int i = 2 * total - 1;
while (HT[i].lchild != -1 || HT[i].rchild != -1) {
if (s[k] == '0') {
i = HT[i].lchild;
}
else {
i = HT[i].rchild;
}
k++;
}
file2.put(HT[i].c);
}
file1.close();
file2.close();
}
运行结果:
每个字符出现的次数和编码:
编号 字符 个数 编码
1 C 24 10100101
2 h 264 11010
3 i 387 0111
4 n 393 1001
5 e 622 001
6 s 346 0101
7 1015 111
8 T 30 11011000
9 a 422 1011
10 u 154 00010
11 l 177 01001
12 t 390 1000
13 r 277 0000
14
31 11011001
15 c 146 110111
16 o 354 0110
17 y 96 101000
18 w 58 1100100
19 m 102 101010
20 - 7 1100001100
21 d 159 00011
22 v 54 1100000
23 z 21 01000101
24 f 103 101011
25 . 59 1100101
26 W 9 1101101110
27 g 110 110001
28 , 53 1010011
29 k 38 0100001
30 B 5 0100000101
31 p 120 110011
32 I 10 010000011
33 ' 11 101001000
34 U 1 1101101101010
35 b 43 0100011
36 ; 2 110110100001
37 q 6 1010010010
38 K 1 1101101101011
39 " 26 11000010
40 j 8 1101101001
41 ( 5 0100010000
42 ) 5 0100010001
43 x 5 0100010010
44 E 2 110110110010
45 P 5 0100010011
46 D 9 1101101111
47 6 1 1101101101100
48 1 1 1101101101101
49 8 1 1101101101110
50 A 7 1100001101
51 9 1 1101101101111
52 0 1 010000010000
53 7 1 010000010001
54 H 9 010000000
55 M 9 010000001
56 L 8 1101101010
57 F 8 1101101011
58 S 15 110000111
59 Y 2 110110110011
60 R 1 010000010010
61 : 4 11011010001
62 X 1 010000010011
63 J 3 10100100110
64 N 4 11011011000
65 Z 2 110110110100
66 V 3 10100100111
67 3 1 110110100000
总结:这道题主要是对哈夫曼编码的具体应用。关于压缩的方式,我只提供了一种比较暴力的方法,但容易理解。大家可以去探索一下更有优的方法。
以上是我的做题经历,很高兴能与大家分享。