距离上次写完哈夫曼编码已经过去一周了,这一周都在写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 = '