哈夫曼树的应用:压缩文件

来源:互联网 发布:newman 网络引论 编辑:程序博客网 时间:2024/06/04 20:18

如果你还对哈夫曼树感到陌生的话,可以去看看这篇博客:
http://blog.csdn.net/fengasdfgh/article/details/52907353

思路:
压缩思路:
由于我们可以利用哈夫曼编码把文档中的每个字符表示为二进制编码.
比如文档里保存一个字符串“abbbcds”,我们可以通过统计每个字符出现的次数来构建哈夫曼树,这样每个字符有唯一的编码,如 a : 100, b : 0, c : 101, d : 110,
s : 111;
这样原字符串可表示为一个二进制序列:100 0 0 0 101 110 111。
接下来我们遍历这个二进制序列并每次以8位 得到一个数并把它作为一个字符写入一个文件,如果二进制序列的长度不是8的整数倍,我们可以补零直到为8的整数倍。
这就是我们的压缩文件。
压缩文件代码:

FILE *trans(const string str)    {        errno_t error;        FILE* file;        error = fopen_s(&file, str.c_str(), "rb");        if (error)        {            throw 1;        }        Node *root = NULL;        //我们构建哈夫曼树需要一个最小堆        Plie<Node*, compare<Node *>> d;        vector<unsigned long > count(256, 0);        //contrat保存着每个字符所对应的编码        map<int, string> contrast;        //统计字符次数        unsigned char va = 0;        while (fread(&va, 1, 1, file) != 0 )        {            count[va]++;        }        fclose(pf);        //将出现过的字符转化为Node        for (int i = 0; i < 256; i++)        {            if (count[i] != 0)            {                Node *p = new Node(i, count[i]);                d.push(p);            }        }        //构建哈夫曼树        root = construct(d);        //记录字符所对应的新编码        Tran(root, contrast);        size_t pos = str.rfind('.');        string s = str.substr(0, pos);        string s1 = s + "configer.txt";        //创建一个配置文件        FILE *fff = NULL;        error = fopen_s(&fff, s1.c_str(), "wb");        if (error)        {            cout << s1.c_str() << ends;            throw 1;        }    //写入配置文件,第一个数为编码总个数        //配置文件格式为:字符 编码        int SIZE = contrast.size();        fprintf(fff, "%d", SIZE);        fputc(' ', fff);        auto end = contrast.end();        for (auto pi  = contrast.begin(); pi != end; pi++)        {            auto p = *pi;            char hp[20] = { 0 };            fprintf(fff, "%c %d", p.first, count[p.first]);            fputc(' ', fff);        }        fclose(fff);        //将原文件字符每个对应的新编码打入到A中,把转换后的数写入到新文件中,压缩文件后缀为zip        //新文件名为原文件名+code;        FILE* f = NULL;        s1 = s + "code.zip";        error = fopen_s(&f, s1.c_str(), "wb");        if (error)        {            cout << s1.c_str() << ends;            throw 1;        }        //写入数据信息    //将所有二进制先写入一个string,如果你    //考虑在文件较大的情况下这样会导致内存不足,可以分批处理        string A;        fseek(file, 0, SEEK_SET);//将原文件指针重新定位到文件开头        while (!feof(file))        {            fread(&va, 1, 1, file);            auto end = contrast[va].end();            for (auto pi  = contrast[va].begin();pi != end; pi++)            {                A.push_back(*pi);            }        }        int cas = 0;        //看A中是否有剩余字节,有的话补至8的整数倍        while (A.size() % 8 != 0)        {            A.push_back('0');        }        //写入信息        int size = A.size();        int sum = 0;        int i = 0;        auto ccend = A.end();        for (auto pi = A.begin(); pi != ccend; pi++)        {            sum = 2 * sum + (*pi - '0');            i++;            //每8个二进制数转换为一个数            if (i % 8 == 0)            {                fwrite(&sum, 1, 1, f);                i = 0;                sum = 0;            }        }        fclose(file);        fclose(f);    }

既然有压缩那么就有解压,接下来就是解压思路:
我们每次从压缩文件里读入一个数,通过一些操作我们可以知道它的每个位的情况,但是这里有问题,我们并不知道哪些位代表着哪个字符,比如我们读入一个值为130的字符,它的二进制为10000010,对于它所代表的数值,我们根本无从知道,我们在这里似乎需要一些帮助,比如:知道原字符所对应的哈夫曼编码。

那么按照上面的思路,我们需要一个配置文件来保存字符所对应的哈夫曼编码。这里有2种思路:

我们可以直接保存哈夫曼编码,那么配置文件里的内容格式为:字符 + 其所对应的哈夫曼编码。

第二种思路,因为哈夫曼编码是由哈弗曼树得来的,而哈夫曼树又是依据字符出现的次数而构建起来的,所以我们只要知道原文件每个字符的出现次数,我们就可以重构出压缩文件时的哈夫曼树。
哈夫曼编码每个数代表着根结点到叶子节点每条路径是向左走向右走,我们知道了哈夫曼编码,那么从根结点开始走,遇到叶子节点就是其所代表的数。

比如如上例:我们读入130,8位二进制序列为10000010,假设我们已经重构了哈夫曼树:
这里写图片描述

**这里有最后一个问题:我们在压缩文件是的最后补零是否会影响到我们解压文件?
可能会(基本上都会),比如上例100 0 0 0 101 110 111,补零后为
100 0 0 0 101 110 1110,我们这是按照上面思路反编码为abbcdsb,而原本为abbcds。
我们可以统计字符的总次数count,每次向文件输入一个值便–.
这里我们就用到了哈夫曼树的特性,哈夫曼树父节点权值 = 左右结点权值之和,所以我们可推出根节点权值为所有叶节点权值之和,这里权值为字符出现次数。
那么根节点的权值为总字符数,我们在重构哈夫曼树后可以获得这个值,**

解压代码:

    //传入的为压缩文件名    void restore(const string str)    {        Plie<Node *, compare<Node *>> A;        int i = 0;        //获取配置文件名        size_t pos = str.rfind('c');        string s = str.substr(0, pos);        string s1 = s + "configer.txt";//将文件的数据读取,分别为新编码个数(即出现字符的数量),并且再读取字符和所对应的次数,同时也放入最小堆中        FILE *fff = NULL;        errno_t error = fopen_s(&fff, s1.c_str(), "rb");        if (error)        {            cout << s1.c_str() << ends;            throw 1;        }        fscanf_s(fff, "%d", &i);        fgetc(fff);        for (int j = 0; j < i; j++)        {            Node *tmp = new Node();            int va = 0;            unsigned char ch = 0;            fscanf(fff, "%c%d",&ch, &va);            fgetc(fff);            tmp->_c = ch;            tmp->_time = va;            A.push(tmp);        }        fclose(fff);        //打开压缩文件        FILE* file = NULL;        error = fopen_s(&file, str.c_str(), "rb");        if (error)        {            cout << str.c_str() << ends;            throw 1;        }        //创立哈夫曼树        Node *root = construct(A);        //获取总个数        unsigned long  long count = root->_time;        //建立一个新文件开始转换,保存为txt格式        pos = str.rfind('c');        s = str.substr(0, pos);        s = s + "1.txt";        FILE *f = NULL;        error = fopen_s(&f, s.c_str(), "wb");        if (error)        {            cout << s.c_str() << ends;            throw 1;        }        //开始转换        Node *head = root;        int  va = 0;        int countsum = 0;        int flag = 0;        int inter = 0;        while (count > 0)        {            fread(&va, 1, 1, file);            //利用pos来获取每个位上的值            int pos = 0x80;            while (0 != pos && count > 0)            {                int turn = (pos & va) ? 1 : 0;                if (0 == turn)                    head = head->_left;                if (0 != turn)                    head = head->_right;                if (NULL == head->_left && NULL == head->_right)                {                    int ch = head->_c;                    fwrite(&ch, 1,1, f);                    count--;                    head = root;                }                pos >>= 1;            }        }        /*fclose(f3);*/        fclose(file);        fclose(f);        /*fclose(fff);*/    }

到这里基本就完成了。
在这里我说明一些会遇到的情况,读取文件时(特别为音频,视频)必须用二进制读取,因为文本读取会在遇到特殊字符而返回EOF,导致文本读取不完,我测试出这个字符ascii码值为26,它就是ctrl+z,即EOF。
二进制读取时注意换行符,会读取出2个值,为13,10.
附:

//哈夫曼树u节店struct HuffNode{    typedef HuffNode Node;    HuffNode()        :_c(0)        , _left(NULL)        , _right(NULL)        , _time(0)        , _parent(NULL)    {};    HuffNode(const char c, const size_t time)        :_c(c)        , _left(NULL)        , _right(NULL)        , _time(time)        , _parent(NULL)    {    }    Node *_left;    Node *_right;    unsigned long   _time;    unsigned char _c;    Node *_parent;};//上文所调用的函数void Tran(Node *root, map<int, string>& contrast)    {        string s;        traning(root, s, contrast);    }void traning(Node *root, string s, map<int, string>& contrast)    {        if (NULL == root->_left && NULL == root->_right)        {            contrast[root->_c] = s;            return;        }        traning(root->_left, s + '0', contrast);        traning(root->_right, s + '1', contrast);    }    Node *construct(Plie<Node*, compare<Node*>>& d)    {        Node *root = NULL;        while (d.size() > 1)        {            //将节点连接起来            root = new Node();            Node *left = root->_left = d.top();            d.pop();            Node *right = root->_right = d.top();            d.pop();            root->_time = left->_time + right->_time;            left->_parent = root;            right->_parent = root;            d.push(root);        }        return root;    }
0 0
原创粉丝点击