距离上次写完哈夫曼编码已经过去一周了,这一周都在写huffman压缩解压,哎,在很多小错误上浪费了很多时间调bug。其实这个程序的最关键部分不是我自己想的,而是借鉴了某位园友的代码,但是,无论如何,自己也是思考,学习,调试了很久,慢慢地清除了一个一个bug。一周的课后时间都花在这上面了,学习了一点东西,对文件的操作也了解了不少,也算有了一点心得吧。下面一一说来吧--
这个程序有三个最关键的点,一个是上次写的哈夫曼编码,那个是基础,另外一个就是位操作,这个是最关键的,具体来讲位操作就是如何把一个01字符串变成内存中实际存放的0和1, 下面还会对这里做出更多解释,还有一个是对文件的操作。
整个程序以二进制的形式读写文件,不是以文本形式读写,因为所有文件在计算机中都是以二进制的形式存放的,以这种最根本的形式能够达到对所有文件操作的目的,当然,文件夹是不可以的,目前对任何形式的单个文件都可以压缩和解压,但是压缩率和文件本身密切相关,不同文件压缩率可能相差很大,甚至某些文件压缩完比原文件还大,这并不是bug,而是在我的这种方法下理论上就可能出现这种情况。另外,该程序只是为了以最简单的方式来实现哈夫曼压缩,所以没仔细考虑时间复杂度,可能压缩某些比较大的视频文件或其他文件需要很长的时间,原因是以二进制的形式读写文件本身就要花费很多时间。想想一个100M的文件都有100 * 1024 *1024 字节,每次读取一个字节,都要上亿次,还要考虑构建哈夫曼树,得到哈夫曼编码,写入文件等等,时间复杂度自然会很高了。这也是我第一次通过自己的实践这么直观地认识到时间复杂度的重要性,也就是算法的重要性。想想我们平时在电脑上用的那些压缩软件,压缩效率是那么高,还特别快,人家肯定用到了很好的算法。
之前我在看C++Primer的时候对流这一章没怎么去看,所以一开始就不知道怎么以二进制的形式操作文件,这里还是要对文件流fstream比较了解才能写这个程序,特别是ifstream的 read()函数以及ofstream的write()函数,怎么去理解fout.write((char *)(&obj), sizeof(obj))中的char*这种强制类型转换呢?这玩意我想了很久,网上看了挺多文章的,没看到有人说过这个,按我的理解,应该就是把任何数据转换成它们在内存中的表示(0和1),然后以字节为单位,把每8位转换成256个ASCII码中的一个字符,然后再写入文件,以这种方式来达到用二进制的形式把任何一种数据写入文件或者从文件读取( fin.read((char *)(&obj), sizeof(obj)) ),然后注意这里的第一个参数必须是引用。还有一个在操作文件流容易错的地方就是操作完记得关闭这个流,断开和相应文件的连接,防止下次另外一个流和同一个文件关联,你不可能对一个文件同时进行读和写的操作。另外对文件流指针,以及二进制文件结束标志都要清楚,很多错误都源于对这些细节不清楚。比如以二进制形式读取时,总发现最后多读了一点东西,仔细调试一下,发现多读的这个东西输出是两个连在一起的空格,查看ASCII发现是十进制255对应的那个字符,八进制为'\377',叫做nbsp,全称是Non-breaking space or no-break space(http://www.theasciicode.com.ar/),在查询相关知识得知它就是任何二进制文件的结束标志,文件流指针读到这个字符时就知道这个文件结束了,这样一来似乎就豁然开朗了,还有得明白文件指针的移动原理,文件指针时刻这个下一个将被操作的字符,每次以字节为单位移动,所以在用到。fin.get()函数时,每次调用ifstream的这个成员函数时,文件指针就向后移动一个字节,所以我们以二进制形式读取一个文件的每一个字符应该这样读
1 while (1)
2 {
3 c = fin.get();//每次以二进制的形式读取八位,并把对应的字符存入c中去
4 if (fin.eof()) break;//读到二进制文件末尾,不再读取
5 freqs[c]++;
6 file_len++; //记录文件的长度,即文件包含多少字节
7 }
而不是
while (!fin.eof())
{
c = fin.get();//每次以二进制的形式读取八位,并把对应的字符存入c中去
freqs[c]++;
file_len++; //记录文件的长度,即文件包含多少字节
}
或者这样也不行
while (1)
{
if (fin.eof()) break;//读到二进制文件末尾,不再读取
freqs[fin.get()]++;
file_len++; //记录文件的长度,即文件包含多少字节
}
想想文件流指针是在执行完fin.get()就移动一次,就能明白了,我们不能把文件的结束标识符也读取进来,另外我们写入的时候也不用自己写入那个结束标识符,因为当ofstream流对象在和它之前的关联的文件断开时就会自动写入一个文件结束标志符。
另外容易犯的一个错误是,由于我每次是以字节为单位按字符的形式读取文件的,这其实就是以二进制操作文件的本质,每次处理8位正好对应256个ASCII码中的一个字符,但是注意这些字符都是unsigned char,也就是无符号的,因为我把字符当成数组下标处理,所以当我最开始读取文件时,每次读取一个字符,添加到一个string,然后直接利用这个得到的string直接调用我上次写的那个
int Create_freq_array(unsigned int (&freqs)[NUM_CHARS],string s, int &char_size)
函数,结果无形中就埋下了一个错误,注意,string中的字符都是char型,是有符号的!当你把一个unsigned char写入string中,也就是如下
while (1)
{
c = fin.get();//每次以二进制的形式读取八位,并把对应的字符存入c中去
if (fin.eof()) break;//读到二进制文件末尾,不再读取
s += c;//string s;
file_len++; //记录文件的长度,即文件包含多少字节
}
这样虽然能利用string记录下原文件读取的字符序列,但是当你调用
int Create_freq_array(unsigned int (&freqs)[NUM_CHARS],string s, int &char_size)//传入数组的引用,
{
int i, maxfreq = 0;
for(int i=0;i<NUM_CHARS;i++)
freqs[i] = 0;//注意传入的数组的各元素先赋值为0
for(auto iter =s.begin(); iter!=s.end(); iter++)
{
freqs[*iter]++; //*iter为char型,这里转换成了int型,即以某个字符的ASCII码作为
if(freqs[*iter] > maxfreq)//它在freq数组中的下标,注意这种方式不能表示非ASCII码字符!
maxfreq = freqs[*iter];//每次记得更新maxfreq的值
}
for(i=0; i<NUM_CHARS; i++)//计算char_size值
{
if(freqs[i])
{
char_size++;
}
}
return 0;
}
假如从文件中读取的某个字符是ASCII码表中的后128个,比如是ASCII为130的那个(10000010,对应字符为 é )那么问题来了,当你写入string中没问题,string能装下这个字符,毕竟string里可以存汉字,也就是说string里是signed char ,那么当你执行红色上面红色标注的那个语句时,你把上面这个字符当成signed char转换成int型的数组下标,那些显然下标是负的,越界了!这个错误我找了很久才找出来,真的一开始没考虑这么多,解决办法为用unsigned char数组来保存从文件中读取的字符,所以改写了代码,没有利用上面写的那个函数。
还有一个错误花了我两天的时间才发现,那就是上一篇最开始定义了一个常量
#define MAX_FREQ 10000 //最大频率必须小于这个数
这次压根没管这个数,结果在压缩解压函数上白浪费了很多时间寻找错误,尾后无意中发现竟然是这个数太小了
发现原因是在某次对某个文件试验调试时注意到这个
这个是
int Build_Huffman_tree(unsigned int (&freqs)[NUM_CHARS],HuffNode (&Huffman_tree_array)[MAX_SIZE],unsigned int n)
中构建完的Huffman_tree_array数组中的最后一个元素,它的左右孩子竟然都是零,我曹,这他么不对呀!依据这个我找到了我寻找两天未果的错误
修改代码
#define MAX_FREQ 100000000 //最大频率必须小于这个数
这下总没事了
其实还有很多小错误,比如数组没有初始化,某个变量没有初始化。。。还是年轻
说完遇到的这么多错误,接下来放代码了,来仔细谈谈如何实现
------------------------------------------------------------ 代码区 -----------------------------------------------------------------------
1 // huffman.cpp : 定义控制台应用程序的入口点。
2 //
3
4 #include "stdafx.h"
5
6 #include <malloc.h>
7 #include <windows.h>
8 #include <string>
9
10 #include "huffman_code.cpp"
11
12
13
14 typedef struct {
15 unsigned char uch; // 以8bits为单元的无符号字符
16 unsigned long freq; // 每类(以二进制编码区分)字符出现频度
17 }TmpNode;
18
19 int Huffman_compress()
20 {
21 string str_read, str_write;//读取和写入的文件名
22 string str_file_name,file_name;//读取的文件的拓展名
23 string buf;// 待存编码缓冲区
24 int flag = 0;
25 unsigned char x;
26 unsigned char char_temp;
27 unsigned char c;
28 unsigned int char_size = 0;//用以保存string中所包含的字符种类
29 int k = 0;
30 float compress_rate;
31 unsigned long file_len = 0;
32 unsigned long file_len2 = 0;
33 unsigned int freqs[NUM_CHARS];
34 HuffNode Huffman_tree_array[MAX_SIZE];
35 HuffCode Huffman_code_array[NUM_CHARS];
36
37 cout << "请输入你所需要压缩的文件名:" << endl;
38 cin >> str_read;
39 ifstream fin(str_read.c_str(), ios::binary);//从str_read中读取输入数据
40
41 //寻找读取的文件的拓展名
42 file_name = str_read;
43 reverse(file_name.begin(), file_name.end());
44 for (auto iter = file_name.begin(); iter != file_name.end(); iter++)
45 {
46 if (*iter == '.')
47 {
48 flag = 1;
49 break;
50 }
51 str_file_name += *iter;
52 }
53 if (flag)//flag为1表示找到了windows系统下的文件拓展名标识符".",也就是说,读取的文件存在拓展名,反转str_file_name
54 reverse(str_file_name.begin(), str_file_name.end());
55 else
56 str_file_name.clear();//读取的文件不存在拓展名,清空str_file_name
57 if (!fin)
58 {
59 cout << "读取文件" << str_read << "失败!" << endl << endl;
60 return 0;
61 }
62 else cout << "请输入压缩后的文件名:" << endl;
63 cin >> str_write;
64 cout << endl;
65 ofstream fout(str_write.c_str(), ios::binary);//向str_write中写入数据
66
67 cout << "压缩中..." << endl;
68 for (int i = 0; i<NUM_CHARS; i++)
69 freqs[i] = 0;//注意传入的数组的各元素先赋值为0
70 while (1)
71 {
72 c = fin.get();//每次以二进制的形式读取八位,并把对应的字符存入c中去
73 if (fin.eof()) break;//读到二进制文件末尾,不再读取
74 freqs[c]++;
75 file_len++; //记录文件的长度,即文件包含多少字节
76 }
77 fin.close();
78 for (int i = 0; i<NUM_CHARS; i++)//计算char_size值
79 {
80 if (freqs[i])
81 {
82 char_size++;
83 }
84 }
85 TmpNode *tmp_nodes = (TmpNode *)malloc(char_size * sizeof(TmpNode));//分配一个实际出现字符种类大小的数组,保存实际出现的字符
86 for (int i = 0; i < 256; ++i)
87 {
88 if (freqs[i])
89 {
90 tmp_nodes[k].uch = i;
91 tmp_nodes[k].freq = freqs[i];
92 k++;
93 }
94 }
95
96 //把拓展名信息写入输出文件
97 x = str_file_name.size();//用一个字节存储文件拓展名的长度,所以要先转换成unsigned char型
98 fout.write((char*)&x, sizeof(unsigned char));
99 if (str_file_name.size())
100 {
101 for (auto iter = str_file_name.begin(); iter != str_file_name.end(); iter++)
102 {
103 x = *iter;
104 fout.write((char*)&x, sizeof(unsigned char));//把出现的唯一字符写入压缩文件
105 }
106 }
107
108 if (char_size == 1)//只有一种字符
109 {
110 fout.write((char*)&char_size, sizeof(unsigned int));//把出现的字符种类写入压缩文件
111 fout.write((char*)&tmp_nodes[0].uch, sizeof(unsigned char));//把出现的唯一字符写入压缩文件
112 fout.write((char*)&tmp_nodes[0].freq, sizeof(unsigned long));//把出现的唯一字符的频率写入压缩文件
113 free(tmp_nodes);
114 }
115 else
116 {
117 Build_Huffman_tree(freqs, Huffman_tree_array, char_size);
118 Huffman_code(Huffman_tree_array, Huffman_code_array, char_size);
119 fout.write((char*)&char_size, sizeof(unsigned int));//把出现的字符种类写入压缩文件
120 for (int i = 0; i<char_size; i++)
121 {
122 fout.write((char*)&tmp_nodes[i].uch, sizeof(unsigned char));//把出现的字符写入压缩文件
123 fout.write((char*)&tmp_nodes[i].freq, sizeof(unsigned long));//把出现的字符的频率写入压缩文件
124 }
125 fout.write((char *)&file_len, sizeof(unsigned long)); // 写入文件长度
126 free(tmp_nodes);
127 ifstream fin2(str_read.c_str(), ios::binary);//从input.txt中再次读取输入数据
128 while (1)
129 {
130 c = fin2.get();
131 if (fin2.eof()) break;
132 char_temp = c;// 每次读取8bits,作为一个字符
133 for (int i = 0; i<char_size; i++)
134 {
135 if (char_temp == Huffman_code_array[i].data)
136 {
137 buf += Huffman_code_array[i].s;
138 break;//匹配上了就跳出循环
139 }
140 }
141 while (buf.size() >= 8)
142 {
143 char_temp = '\0'; // 清空字符暂存空间,改为暂存字符对应编码
144 for (auto iter = buf.begin(); iter<buf.begin() + 8; iter++)
145 {
146 char_temp <<= 1; // 左移一位,为下一个bit腾出位置
147 if (*iter == '1')
148 char_temp |= 1; // 当编码为"1",通过 位或 操作符将其添加到字节的最低位
149 }
150 fout.write((char *)&char_temp, sizeof(unsigned char)); // 将字节对应编码存入文件
151 buf = buf.substr(8);// 编码缓存去除已处理的前八位
152 }
153 }
154 // 处理最后不足8bits编码
155 if (buf.size() > 0)
156 {
157 char_temp = '\0';
158 for (auto iter = buf.begin(); iter != buf.end(); iter++)
159 {
160 char_temp <<= 1;
161 if (*iter == '1')
162 char_temp |= 1;
163 }
164 char_temp <<= 8 - buf.size(); // 将编码字段从尾部移到字节的高位
165 fout.write((char *)&char_temp, sizeof(unsigned char)); // 存入最后一个字节
166 }
167 fin2.close();
168 }
169 fout.close();
170 ifstream fin3(str_write.c_str(), ios::binary);
171 while (1)
172 {
173 c = fin3.get();//每次以二进制的形式读取八位,并把对应的字符存入c中去
174 if (fin3.eof()) break;//读到二进制文件末尾,不再读取
175 file_len2++; //记录文件的长度,即文件包含多少字节
176 }
177 fin3.close();
178 compress_rate = float(file_len2) / float(file_len);
179 cout << "压缩完成!" << endl << endl;
180 cout << "压缩之前的文件大小为:" << file_len << "字节" << endl;
181 cout << "压缩之后的文件大小为:" << file_len2 << "字节" << endl;
182 cout << "压缩率为:" << compress_rate << endl << endl;
183 return 0;
184 }
185
186 int Huffman_uncompress()
187 {
188 string str_read, str_write;//读取和写入的文件名
189 string str_file_name; //待解压的文件的拓展名
190 unsigned int char_size,root;
191 unsigned char code_temp;
192 unsigned char x;
193 int k;
194 unsigned long file_len;
195 unsigned long writen_len = 0;
196 unsigned int freqs[NUM_CHARS];
197 HuffNode Huffman_tree_array[MAX_SIZE];
198 cout << "请输入你所需要解压的文件名:" << endl;
199 cin >> str_read;
200 ifstream fin(str_read.c_str(), ios::binary);//从str_read中读取输入数据
201 if (!fin)
202 {
203 cout << "读取文件" << str_read << "失败!" << endl << endl;
204 return 0;
205 }
206 else cout << "请输入你解压后的文件名:" << endl;
207 cin >> str_write;
208 cout << endl;
209 cout << "解压中..." << endl;
210
211 //读取文件拓展名信息
212 fin.read((char*)&x, sizeof(unsigned char));
213 k = x;
214 if (k)
215 {
216 for (int i = 0; i < k; i++)
217 {
218 fin.read((char*)&x, sizeof(unsigned char));
219 str_file_name += x;
220 }
221 }
222
223 str_write += '.';//把拓展名信息写入输出文件名
224 str_write += str_file_name;
225 ofstream fout(str_write.c_str(), ios::binary);//向str_write中写入数据
226 fin.read((char*)&char_size, sizeof(unsigned int));//读取字符种类
227 if (char_size == 1)
228 {
229 fin.read((char *)&code_temp, sizeof(unsigned char));// 读取唯一的字符
230 fin.read((char *)&file_len, sizeof(unsigned long)); // 读取文件长度
231 while (file_len--)
232 fout.write((char *)&code_temp, sizeof(unsigned char));
233 }
234 else
235 {
236 for (int i = 0; i<NUM_CHARS; i++)
237 freqs[i] = 0;//注意传入的数组的各元素先赋值为0
238 for (int i = 0; i<char_size; i++)
239 {
240 fin.read((char*)&code_temp, sizeof(unsigned char));//把当前字符从压缩文件中读取出来
241 fin.read((char*)&file_len, sizeof(unsigned long));//把当前字符的频率从压缩文件中读取出来
242 freqs[code_temp] = file_len;
243 }
244 Build_Huffman_tree(freqs, Huffman_tree_array, char_size);//根据压缩文件头一段信息重建哈夫曼树
245 root = 2 * char_size - 2;//根结点在树数组中的下标
246 fin.read((char *)&file_len, sizeof(unsigned long)); // 读入文件长度
247
248 while (1)
249 {
250 fin.read((char *)&code_temp, sizeof(unsigned char));// 读取一个字符长度的编码
251
252 // 处理读取的一个字符长度的编码(通常为8位)
253 for (int i = 0; i < 8; ++i)
254 {
255 // 由根向下直至叶节点正向匹配编码对应字符
256 if (code_temp & 128)//按位与,是压缩时按位或的逆过程,code_temp字符对应的二进制的最高位是1,往右孩子方向走
257 root = Huffman_tree_array[root].rchild;
258 else
259 root = Huffman_tree_array[root].lchild;
260
261 if (root < char_size)//到达叶子结点
262 {
263 fout.write((char *)&Huffman_tree_array[root].data, sizeof(unsigned char));
264 ++writen_len;//记录读取的文件长度
265 if (writen_len == file_len) break; // 控制文件长度,跳出内层循环
266 root = 2 * char_size - 2; // 复位为根索引,匹配下一个字符
267 }
268 code_temp <<= 1; // 将编码缓存的下一位移到最高位,供匹配
269 }
270 if (writen_len == file_len) break; // 控制文件长度,跳出外层循环
271 }
272 }
273 fin.close();
274 fout.close();
275 cout << "解压完成!" << endl << endl;
276 return 0;
277 }
278
279 int main()
280 {
281 int a;
282 while (1)
283 {
284 cout << "请选择你的操作:" << endl;
285 cout << "1 压缩文件" << endl;
286 cout << "2 解压文件" << endl;
287 cout << "3 退出程序" << endl;
288 cout << "4 清理屏幕" << endl;
289 cin >> a;
290 cout << endl;
291 switch (a)
292 {
293 case 1: Huffman_compress(); break;
294 case 2: Huffman_uncompress(); break;
295 case 3: exit(0);
296 case 4: system("cls");
297 }
298 }
299 return 0;
300 }
调试工具为VS2017,所以会有 #include "stdafx.h"
另外,#include "huffman_code.cpp" 就是上次写的那个程序,改了一点东西,全在这里面(http://files.cnblogs.com/files/journal-of-xjx/huffman.rar)
这是压缩文件的存储结构
拓展名长度 | 拓展名 。。。 | 字符种类 | 字符 字符频率 。。。 | 文件长度 | 编码 。。。 |
unsigned char | unsigned char 。。。 | unsigned int | unsigned char unsigned long 。。。 | unsigned long | unsigned char 。。。 |
省略号表示该部分有一系列值,另外,如果拓展名长度为0,那么不存储拓展名,如果文件种类为1,那么不存储文件长度,解压文件时按照压缩时的存储顺序依次读取各部分的值
整个程序最关键的两部分,一个是压缩函数中的:
1 while (buf.size() >= 8)
2 {
3 char_temp = '\0'; // 清空字符暂存空间,改为暂存字符对应编码
4 for (auto iter = buf.begin(); iter<buf.begin() + 8; iter++)
5 {
6 char_temp <<= 1; // 左移一位,为下一个bit腾出位置
7 if (*iter == '1')
8 char_temp |= 1; // 当编码为"1",通过 位或 操作符将其添加到字节的最低位
9 }
10 fout.write((char *)&char_temp, sizeof(unsigned char)); // 将字节对应编码存入文件
11 buf = buf.substr(8);// 编码缓存去除已处理的前八位
12 }
13
14
15 if (buf.size() > 0)
16 {
17 char_temp = '\0';
18 for (auto iter = buf.begin(); iter != buf.end(); iter++)
19 {
20 char_temp <<= 1;
21 if (*iter == '1')
22 char_temp |= 1;
23 }
24 char_temp <<= 8 - buf.size(); // 将编码字段从尾部移到字节的高位
25 fout.write((char *)&char_temp, sizeof(unsigned char)); // 存入最后一个字节
26 }
这里用到了按位或操作和移位操作,具体如下:
假如buf现在的内容是 "0101000111010",那么我们先处理前八个字符"01010001",依次从左到右读取这个字符串,如果读到1,那么char_temp = char_temp | 1;
char_temp每次初始化为"00000000",1对应的ASCII码为"00000001",第一次读到0,char_temp先左移一位,char_temp不执行按位或,还是"00000000",第二次读到1,此时按位或,char_temp更新为"00000001",然后左移一位,保持char_temp的最低为为0,然后右读取一个字符,判断是否执行char_temp = char_temp | 1,八次循环下来,char_temp的值对应的ASCII码变为了01010001,这正好是我们最开始处理的buf中的前八个字符,换句话说,我们巧妙地利用char_temp与数字1的按位或以及移位操作成功将含有8个01字符串转换成一个unsigned char 的ASCII码的8个0和1。最后处理最后剩下的不足8个字符的01串,注意这次移位不是移一位而是 8 - buf.size() 位,把有效信息放前面,无效的0放后面。
二是解压函数中的:
1 for (int i = 0; i < 8; ++i)
2 {
3 // 由根向下直至叶节点正向匹配编码对应字符
4 if (code_temp & 128)//按位与,是压缩时按位或的逆过程,code_temp字符对应的二进制的最高位是1,往右孩子方向走
5 root = Huffman_tree_array[root].rchild;
6 else
7 root = Huffman_tree_array[root].lchild;
8
9 if (root < char_size)//到达叶子结点
10 {
11 fout.write((char *)&Huffman_tree_array[root].data, sizeof(unsigned char));
12 ++writen_len;//记录读取的文件长度
13 if (writen_len == file_len) break; // 控制文件长度,跳出内层循环
14 root = 2 * char_size - 2; // 复位为根索引,匹配下一个字符
15 }
16 code_temp <<= 1; // 将编码缓存的下一位移到最高位,供匹配
17 }
这里用到了按位与操作和移位操作,目的和上面正好相反,是把ASCII中的0和1转换成字符串0和1来匹配建立好的huffman编码信息,还原相应字符。
这一部分代码借鉴了@http://www.cnblogs.com/keke2014/p/3857335.html的代码
还有一些地方也参考了他的思路,万分感谢!
编程需细心,很多你不注意的细节往往隐藏着你需要几天才能找出来的bug
---XJX
---17.4.11 江大 桃园