Huffman编码与解码(实现任意文章内容压缩)

[问题描述]

对一篇不少于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

总结:这道题主要是对哈夫曼编码的具体应用。关于压缩的方式,我只提供了一种比较暴力的方法,但容易理解。大家可以去探索一下更有优的方法。

以上是我的做题经历,很高兴能与大家分享。

  • 4
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值