huffman压缩解压文件

距离上次写完哈夫曼编码已经过去一周了,这一周都在写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 = '';        // 清空字符暂存空间,改为暂存字符对应编码
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 = '';
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" 就是上次写的那个程序,改了一点东西,全在这里面(https://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 = '';        // 清空字符暂存空间,改为暂存字符对应编码
 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 = '';
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

---2017.4.11 江大 桃园 

 
只有0和1的世界是简单的
原文地址:https://www.cnblogs.com/nullxjx/p/6695463.html