用Huffman树实现文件压缩与解压

来源:互联网 发布:java学生管理系统集合 编辑:程序博客网 时间:2024/06/01 09:03

用Huffman树实现文件的压缩与解压
我们先来了解一下什么是Huffman树?
我们平常所使用的Zip等压缩工具都是借助Huffman树实现的,Huffman是一种特殊的二叉树,它是一种加权路径最短的二叉树,
因此也称为最优二叉树。
(下面用一幅图来说明)
这里写图片描述
它们的带权路径长度分别为:

图1: WPL=3*2+4*2+2*2+10*2=48
图2: WPL=3*3+2*3+4*2+10*1=33
我们很容易可以看出图2的带权路径长度较小,证明图b就是哈夫曼树(也称为最优二叉树)。

如何构建一个Huffman树?
一般我们可以按下面步骤构建:
1)将所有左,右子树都为空的作为根节点。
2)在森林中选出两棵根节点的权值最小的树作为一棵新树的左,右子树,且置新树的附加根节点的权值为其左,右子树上根节点的权值之和。注意,左子树的权值应小于右子树的权值。
3)从森林中删除这两棵树,同时把新树加入到森林中。
4)重复2,3步骤,直到森林中只有一棵树为止,最终生成的树便是哈夫曼树
(Huffman的构建图解如下):
这里写图片描述
Huffman有什么特点呢?

我们构建的Huffman树,保证了所有的成员数据都在叶子节点上,最底层的是最小的两个节点数据,向顶端求和,然后再依次增加一个次小的
节点数据再求和,知道数据被全部插入Huffman树中,这样一来这个树中的节点就有了一个特点,离根节点越近的节点数据越大,
离根节点越远的节点数据越小。

用Huffman树实现文件压缩流程:

1.构造一个存放该文件中字符出现的信息的结构体,该结构体的成员分别有字符、字符出现的次数,对该字符的编码。
2.创建一个字符信息的结构体数组,读要压缩的文件,统计给文件的字符信息写入该数组中。
3.根据该字符数组,按照字符出现的次数构造一个Huffman,由于Huffman树的构建每次取最小数据的特点,所以我们借助构建一个小顶堆来实现。
4.根据该Huffman对每个字符进行编码,将每个字符的编码写入字符结构体数组中。(这里我们用图示具体说明一下)
这里写图片描述
5.对该文件进行压缩,读取文件信息,根据读取到的字符压缩为该字符对应的编码,编码够8位写入压缩文件,读完文件中的所有内容,压缩完成。
需要注意的是什么时候就完成了对整个文件的压缩呢?这个问题其实我们可以有多种方法,我们在第一次读取源文件的时候就可以增加一个
计数器来记录当前文件的字符总数,另外其实如果我们足够熟悉Huffman树,很容易就可以看出来,Huffman数的根节点有什么意义?
没有错,根节点的值是从底端到顶端一层层累加上去的,因此根节点的数据就保存着文件的总字符数。

压缩源码:

    string Compress(const char* filename)    {        // 1.统计字符出现的次数        assert(filename);        FILE* fout = fopen(filename, "rb");                //以只读方式打开文件        assert(fout);        //读取文件中的字符          int ch = fgetc(fout);        while (ch != EOF)        {            _Infos[ch]._count++;            ch = fgetc(fout);        }        //fclose(fout);        // 2.生成huffman tree        CharInfo invaild;        HuffmanTree<CharInfo> Tree(_Infos, 256,invaild);        // 3.生成huffmn code        string code;        _GenerateHuffmanCode(Tree.ReturnRoot(), code);        Node* Root = Tree.ReturnRoot();        cout <<"压缩"<< Root->_Weight._count <<"字符"<<endl;        //4.压缩        string CompressFilename = filename;        CompressFilename.append(".Compress");        FILE* fin = fopen(CompressFilename.c_str(), "wb");        fseek(fout, 0, SEEK_SET);   //将文件偏移量设置为0        ch = fgetc(fout);        unsigned char value = 0;        int pos = 0;        while (ch != EOF)        {            code = _Infos[ch]._code;            for (size_t i = 0; i < code.size(); i++)            {                //value &= (code[i]&1);                //value <<= 1;                if (code[i] == '1')                {                    value <<= 1;                    value |= 1;                }                else                {                    value <<= 1;                    value |= 0;                }                pos++;                if (pos == 8)                {                    pos = 0;                    fputc(value, fin);                    value = 0;                }            }            ch = fgetc(fout);        }        if (pos != 0)        {            value <<= (8 - pos);            fputc(value, fin);        }        cout << "文件压缩:" << filename<<endl;        /*cout << "一共压缩:" <<Tree.ReturnRoot()._Weight._count << "字符" << endl;*/        fclose(fin);        fclose(fout);        //创建配置文件,配置文件存储源文件中字符和字符所出现的次数        string  ConfigFilename = filename;        ConfigFilename += ".config";        FILE* finConfig = fopen(ConfigFilename.c_str(), "wb");        string Line;        char Buff[128];        for (int i = 0; i < 256; i++)        {            if (_Infos[i]._count != 0)            {                /*Line += _Infos[i]._ch;*/                fputc(_Infos[i]._ch, finConfig);                /*Line += ',';*/                Line = Line + ',';                //Line += itoa(_Infos[i]._count, Buff, 10);                _itoa((int)_Infos[i]._count, Buff, 10);                Line += Buff;                Line += '\n';                fputs(Line.c_str(), finConfig);                Line.clear();            }        }        fclose(finConfig);        return CompressFilename;    }

这个时候我们就可以来实现解压缩部分了,那么如何对压缩文件进行解压缩呢?

6.首先解压缩时需要一个配置文件,该文件是源文件的字符信息,需要造次构建一个Huffman,来对应完成文件解压,为什么需要这个配置文件?
原因很简单,我们压缩的时候构建了一个Huffman树并对每个字符进行了编码,我们是按照这种规则去压缩的,因此在解压缩的时候我们同样
需要这样Huffman树来帮助我们完成解压缩,正因为这样的压缩与解压缩的对应关系,我们实现了对源文件准确无误的还原。

7.读取压缩文件,当我们读取到码’1’时指向Huffman树的指针向右移动,当我们读取到码’0’时指针向左移动,由于每个字符对应的都是叶子结点,因此
我们只需要判断当前指针的Left和Right是否为空,为空则将该段码字译为对应的字符即可。循环读取解压,直到文件结尾即实现了对整个文件的解压缩。

解压缩源码:

string UnCompress(const char* filename)                 //解压缩    {        assert(filename);        //打开配置文件        string name= filename;        size_t Config_index = name.rfind('.');        string ConfigFileName = name.substr(0, Config_index);        ConfigFileName += ".config";        FILE* foConfigFilename = fopen(ConfigFileName.c_str(),"rb");        cout << "解压缩文件:" << name << endl;        //重新构建_Infos 哈希表        string Line;        while (ReadLine(foConfigFilename, Line))        {            unsigned char ch = Line[0];            _Infos[ch]._ch = Line[0];            LongType count = atoi(Line.substr(2).c_str());            _Infos[ch]._count = count;            Line.clear();        }        //消除压缩文件的后缀(.Compress)        string CompressFilename = filename;        size_t index=CompressFilename.rfind('.');        string UnCompressFilename = CompressFilename.substr(0, index);        UnCompressFilename.append(".UnCompress");        // 生成huffman tree        CharInfo invaild;        HuffmanTree<CharInfo> Tree(_Infos, 256, invaild);        //打开要被解压缩的文件        FILE* fout = fopen(filename, "rb");        //创建解压缩文件,并写入        FILE* fin = fopen(UnCompressFilename.c_str(), "wb");        Node* root = Tree.ReturnRoot();        Node* cur = root;        LongType count = root->_Weight._count;        cout << "解压缩" << count << "字符" << endl;        unsigned char ReadCh = fgetc(fout);        int pos = 7;        while (ReadCh != EOF)        {            if (pos >= 0)            {                if (((ReadCh >> pos) & 1) == 1)                {                    cur = cur->_Right;                }                else                {                    cur = cur->_Left;                }                --pos;                if (cur->_Weight == NULL && cur->_Right == NULL&& count > 0)                {                    fputc(cur->_Weight._ch, fin);                    cur = root;                    --count;                }                if (count <= 0)                {                    break;                }            }            else            {                ReadCh = fgetc(fout);                pos = 7;            }        }        return UnCompressFilename;    }

下面我们来看一下压缩和解压缩的结果如何?
这里写图片描述
上图中名为src_file的是我们解压的源文件。
后缀为.Compress的是生成的压缩文件,我们对比一下源文件可以看到文件大小明显从4KB缩小到了3KB因为我的测试源文件里面写了一小部分内容,文件大小不是足够的大,所以解压效果不是很明显,大家可以尝试解压一个相对较大的文件,对比一下结果。
第三个后缀为.txt的为为解压专门生成的配置文件。
最后一个后缀为.UnCompress的文件是完成的解压缩文件。打开该文件内容和源文件完全相同。

到这我们就顺利地完成了基于Huffman树的文件压缩与解压缩,在实现该功能过程中我遇到了一些问题和大家分享一下:
(1)、读写文件的打开方式,我们打开文件时一定要使用二进制形式”wb”,”rb”打开需要读写的文件。文本形式打开会对文件中的一些字符做特殊处理,这样会导致不能正确无误地解压文件。
(2)、本次Huffman树实现文件的压缩与解压程序用C++模板编程,由于模板不能分离编译,因此跨文件应用程序时需要将头文件写为.hpp后缀方可通过编译。

原创粉丝点击