文件压缩---huffman算法

来源:互联网 发布:大智慧软件 编辑:程序博客网 时间:2024/06/05 04:59

万年历online项目链接点击这里

本篇文章主要包含4个方面:
1.哈夫曼算法实现压缩的原理
2.具体压缩及解压过程思路阐述
3.项目中遇到的问题
4.项目扩展

一、原理简述:
huffman算法实现文件压缩的主要原理是通过huffman编码来重新表示字符,使得出现频率高的字符编码短,出现少的字符编码长。当用编码表示原文件时,总体的bit位时相对减少的。但当大部分字符出现的频率都差不多时,huffman压缩的压缩效率会很低。

二、具体压缩及解压思路:
压缩:
1. 统计字符出现的次数
由于所有的文件在电脑中都是以二进制的形式存储的,打开文本图片音乐视频的工具就是一种解码的过程,打开不同的文件则依据的是不同的解码规则。例如,图片是由一个个像素构成的,像素呢又是由二进制组成,所以同一样可以使用ASCII码保存信息。
我们使用一个容量为256个元素的数组来统计字符出现的次数,通过结构体将次数、字符、huffman编码相对应。

2.构建哈夫曼树
采用哈夫曼算法,将一组集合中权值最小的两棵树拿出来,以他们的权值之和作为父节点插入到这个集合中,不断重复,直到集合中只有一棵树。这棵树就是为哈夫曼树,再次我们采用最小堆来寻找这两个最小的数。在此我们以字符出现次数作为权值构建哈夫曼树,这样一来出现次数越多的字符就越接近根结点。

3.得到哈夫曼编码
有了哈夫曼树,我们通过从根结点出发走到各个叶子节点,向左走为0,向右走为1,就得到每个字符的哈夫曼编码。因此我们如果有哈夫曼树就可以将哈夫曼编码翻译为具体字符。

4.压缩
先写入配置信息,由于在解压时是没有原文件的,因为我们的压缩文件里存储的是哈夫曼编码,要接压必须先构建哈夫曼树,从而才能将哈夫曼编码通过发福曼树解释为字符。哈夫曼树是根据字符及次数构建的,所以我们存入字符和对应次数即可。
重新读取源文件,逐个将字符转换为对应的哈夫曼编码存放到压缩文件中。

解压缩(原理和压缩相似):

1.打开压缩文件并读取配置信息;
2.根据配置信息,建立哈夫曼树;
3.解压缩
在压缩文件中逐个字符读取,解析该字符的每一位,才用贪心法,只要遇到一个叶子节点就还原对应的字符,并将该字符存放到解压缩的文件里面。此时应注意,压缩文件的最后几位可能是我们补上去的,通过哈夫曼树性质可知,根结点的权值就是所有字符出现的总次数,我们可以借总次数来控制解压。

三、在项目中遇到的问题
1.解压时解压不完全
当用文本方式读取压缩文件时,由于是以哈夫曼编码(二进制数)存储,用EOF(宏,定义为-1)判断文件结尾时有可能提前遇到文件结束标志,因为二进制文件中-1是可以实现的。所以应采用二进制形式打开并使用 feof() 函数判断文件结束。
如果以文本方式读取,要把 ‘\r’(回车)、‘\n’(换行)两个字符转换为一个字符\n,而二进制形式则不需要处理。

2.二次压缩效率低
因为压缩之后,配置信息中字符出现的次数都相差不大,体现不出来哈夫曼的特性,所以再压缩的话效率会非常底。

3.汉字压缩时出现问题
由于汉字是用多个字符表示的,这些字符的范围是0-255,所以应该用 unsigned char 声明读取的字符。

四、项目扩展
实现了对文件夹的压缩,对文件夹实际上就是对文件夹中的内容进行压缩,所以找到一个子文件后就一直向里找,直到对找到的文件进行压缩。

MyHeap.h

#pragma once#include<iostream>#include<assert.h>#include<vector>using namespace std;//堆排序//仿函数 定义排列的方法,实现排列代码的复用template<class T>struct UpOrder{    bool operator()(const T i, const T j)    {        return i < j;    }};template<class T>struct DownOrder{    bool operator()(const T i, const T j)    {        return i > j;    }};//堆排列,默认升序排列方法,即排列成小堆template<class T, class Compare = DownOrder<T>>class Heap{public:    Heap()    {}    Heap(T*arr, int size)    {        //将数组中的顺序插入顺序表中        _a.reserve(size);        for (int i = 0; i < size; i++)        {            _a.push_back(arr[i]);        }        //建堆        for (int i = (size - 2) / 2; i >= 0; i--)        {            AdJustDown(i, size);        }    }    void Sort(T* arr, int size)    {        assert(arr);        int i = (size - 2) >> 1;//找到倒数第一个非叶节点        for (; i >= 0; i--)//从倒数第一个非叶节点开始往上排序        {            AdJustDown(i, size);        }    }    void Push(const T &a)    {        _a.push_back(a);        AdJustUp(_a.size()-1);    }    size_t Size()    {        return _a.size();    }    void Printf()    {        for (size_t i = 0; i < _a.size(); i++)        {            cout << _a[i] << " ";        }        cout << endl;    }    const T& Top()    {        return _a[0];    }    void Pop()//将堆顶元素和最后一个元素交换,删除最后一个,再调整顺序    {        assert(!_a.empty());        swap(_a[0], _a[_a.size() - 1]);        _a.pop_back();        if (_a.size() > 1)        {            AdJustDown(0, _a.size());        }    }protected:    void AdJustDown(int root, int size)    {        assert(!_a.empty());        int parent = root;      //用parent指针接收要排序的根结点        int child = parent * 2 + 1; //child表示该parent的左孩子结点        while (child < size)        {            //如果右孩子存在并且左孩子的值大于右孩子,则让child指针指向右孩子            if (((child + 1) < size) && Compare()(_a[child], _a[child + 1]))            {                child++;            }            if (Compare()(_a[parent], _a[child]))   //若parent大则交换父子结点的值             {                swap(_a[parent], _a[child]);                parent = child;     //继续往下比较                child = 2 * parent + 1;            }            else            {                break;            }        }    }    void AdJustUp(int child)    {        assert(!_a.empty());        while (child>0)        {            int parent = (child-1)>>1;//找到倒数第一个非叶节点            if (Compare()(_a[parent], _a[child]))            {                swap(_a[parent], _a[child]);                child = parent;            }            else            {                break;            }        }    }private:    vector<T> _a;};

FileCompress.h

    #pragma once    #include<string>    #include<algorithm> //算法头文件,可直接使用reverse函数    #include<io.h>    #include<direct.h>    #include "MyHufmanTree.h"    typedef long long LongType;    //定义结构体存放字符信息    struct FileInfo    {        FileInfo(LongType appearCount = 0)        :_appearCount(appearCount)        {}        FileInfo operator+(const FileInfo &info)const        {            return FileInfo(_appearCount + info._appearCount);        }        bool operator != (const FileInfo &info)const        {            return _appearCount != info._appearCount;        }        bool operator == (const FileInfo &info)const        {            return _appearCount == info._appearCount;        }        bool operator<(const FileInfo &info)const        {            return _appearCount < info._appearCount;        }        bool operator>(const FileInfo &info)const        {            return _appearCount > info._appearCount;        }        unsigned char _ch;  //字符        LongType _appearCount;//字符出现的次数        string _strCode;    //字符对应的哈夫曼编码,定义为字符串    };    class HuffCompressFile    {    public:        struct _HuffmanInfo        {            unsigned char _ch;            LongType _count;        };        const string Compressfile(string filename)  //传入要压缩的文件名,返回已压缩的文件名        {            vector<string>file;            string path = filename.c_str();//c_str()函数返回一个指向该字符串的指针常量            getFiles(path, file);//把各级文件夹信息保存到file数组中            if (file.empty())//如果为空,则表示是一个文件,直接压缩            {                return _Compress(filename);            }            else    //文件夹            {                //首先创建一个新的文件夹                string newpath = path;  //新的压缩后文件夹的路径名                newpath += ".huf";                _mkdir(newpath.c_str());                for (int i = 0; i < (int)file.size(); i++)                {                    _Compress(file[i], newpath);                }                return newpath;     //返回新建的压缩文件夹的名字            }        }        const string _UnCompressfile(string filename)           {            vector<string>file;            string path = filename.c_str();//c_str()函数返回一个指向该字符串的指针常量            getFiles(path, file);//把各级文件夹信息保存到file数组中            if (file.empty())//如果为空,则表示是一个文件进行压缩            {                return _UnCompress(filename);            }            else    //文件夹            {                //首先创建一个新的文件夹                string newpath = filename;  //新文件夹                for (int i = (int)filename.size() - 1; i >= 0; i--)                {                    if (filename[i] == '.')                    {                        newpath.resize(i);                        break;                    }                }                newpath += ".uhuf";                _mkdir(newpath.c_str());    //创建一个新的解压缩文件                for (int i = 0; i < (int)file.size(); i++)                {                    _UnCompress(file[i], newpath);                }                return newpath;     //返回新建的解压缩文件夹的名字            }        }    protected:        //初始化所有字符        void HuffFileCompress()        {            for (size_t index = 0; index < 256; ++index)            {                _fileInfo[index]._ch = index;                _fileInfo[index]._appearCount = 0;            }        }        const string _Compress(const string filename, const string path = string())//        {            HuffFileCompress();//初始化结点            //1.获取源文件中每个字符出现的次数            FILE* fp = fopen(filename.c_str(), "rb");            assert(fp);            unsigned char ch = fgetc(fp);            assert(ch);            while (!feof(fp))            {                _fileInfo[ch]._appearCount++;                ch = fgetc(fp);            }            //2.根据字符出现的次数构建哈夫曼树,树中得体现出字符和次数            FileInfo invalid;            invalid._appearCount = 0;//出现0次的字符不用来创建哈夫曼树            HuffmanTree<FileInfo> hf(_fileInfo, 256, invalid);            //3.通过哈夫曼树获得每个字符的哈夫曼编码,遍历到叶子结点即可得到该结点哈夫曼编码            //由于需要从树顶向根部遍历,所以需要parent指针,即树节点是三岔链            HuffmanStrCode(hf.Top());            //4.压缩(遍历文档,字符转换为strcode) 压缩文件后缀定为.huf            FILE* fIn = NULL;            string CompressFileName = filename;            CompressFileName += ".huf";            if (path.empty())   //为空表示是单个文件            {                //第二次fOut打开 filename.huf                fIn = fopen(CompressFileName.c_str(), "wb");                assert(fIn);            }            else  //不为空表示是文件夹            {                //得到要创建的路径                string FileName;    //得到文件名                int i = filename.size() - 1;                for (; i >= 0; i--)                {                    if (filename[i] == '\\')                        break;                    FileName += filename[i];                }                reverse(FileName.begin(), FileName.end());                string newpath = path;                newpath += '\\';                newpath += FileName;                newpath += ".huf";                fIn = fopen(newpath.c_str(), "wb");  //打开压缩文件                assert(fIn);            }            //写配置信息(字符出现的次数),解压时根据配置信息方能正确解压            _HuffmanInfo info;            for (size_t i = 0; i < 256; ++i)            {                if (_fileInfo[i]._appearCount)//出现过的以结构体的形式直接写进去                {                    info._ch = _fileInfo[i]._ch;                    info._count = _fileInfo[i]._appearCount;                    size_t size = fwrite(&info, sizeof(_HuffmanInfo), 1, fIn);                    assert(size = sizeof(_HuffmanInfo));                }            }            //如何知道写了多少个info进去? 区分配置信息和压缩信息            info._count = 0;//已写的info count肯定不为0,用来隔离配置信息和压缩信息            fwrite(&info, sizeof(_HuffmanInfo), 1, fIn);            unsigned char value = 0;            int count = 0;            fseek(fp, 0, SEEK_SET); //fp指向文件的开始            ch = fgetc(fp); //逐个字符读取            assert(ch);            while (!feof(fp))            {                string &code = _fileInfo[ch]._strCode;                for (size_t i = 0; i < code.size(); i++)                {                    value <<= 1;                    if (code[i] == '1')                    {                        value |= 1;                    }                    else                    {                        value |= 0;                    }                    ++count;                    if (count == 8)//满8位写入一次                    {                        fputc(value, fIn);                        value = 0;                        count = 0;                    }                }                ch = fgetc(fp);            }            if (count != 0) //            {                value <<= (8 - count);                fputc(value, fIn);            }            fclose(fIn);            fclose(fp);            return CompressFileName;        }        const string _UnCompress(const string filename,const string path = string())        {            //得到解压缩之后的文件的名字            string name;            name = filename;            int i = 0;            string posfix;            for (i = (int)filename.size() - 1; i >= 0; --i) //找到后缀出现的位置            {                posfix.push_back(filename[i]);                if (filename[i] == '.')                    break;            }            reverse(posfix.begin(), posfix.end());//让posfix保存要解压文件的后缀            if (posfix != ".huf")   //如果要解压的不是huffman压缩的则不能解压            {                return string();            }            //1.改变文件后缀              name.resize(i);             string UnCompressFileName = name;   //得到压缩文件名            UnCompressFileName += ".unhuf";            FILE *fInput = fopen(filename.c_str(), "rb");            assert(fInput);            FILE *fOut = NULL;              //解压缩文件            if (path.empty())             //如果为空,表示是单个文件解压            {                //打开解压缩文件                fOut = fopen(UnCompressFileName.c_str(), "wb");                if (fOut == NULL)                {                    fclose(fInput);                    exit(EXIT_FAILURE);                }            }            else                           //文件夹进行解压缩            {                string FileName;         //先得到压缩的文件名                for (int i = (int)name.size() - 1; i >= 0; i--)                {                    if (name[i] == '\\')                    {                        break;                    }                    FileName += name[i];                }                reverse(FileName.begin(), FileName.end());                string newpath = path;                newpath += "\\";                newpath += FileName;                newpath += ".uhuf";                //打开解压缩文件                fOut = fopen(newpath.c_str(), "wb");                if (fOut == NULL)                {                    fclose(fInput);                    exit(EXIT_FAILURE);                }            }            //2.重建哈夫曼树(根结点是所有叶子结点的和)            //先读取字符次数信息,再重建哈弗曼树            FileInfo UnComInfo[256];            _HuffmanInfo info;            while (1)            {                size_t size = fread(&info, sizeof(_HuffmanInfo), 1, fInput);                assert(size = sizeof(_HuffmanInfo));                if (info._count > 0)                {                    UnComInfo[(unsigned char)info._ch]._ch = info._ch;//可能出现汉字,所以用unsigned char                    UnComInfo[(unsigned char)info._ch]._appearCount = info._count;                }                else                {                    break;                }            }            FileInfo invalid;            invalid._appearCount = 0;//出现0次的字符不用来创建哈夫曼树            HuffmanTree<FileInfo> tree(UnComInfo, 256, invalid);            HuffmanTreeNode<FileInfo>* root = tree.Top();            LongType charCount = root->_w._appearCount; //所有结点出现的次数,即有效字符出现的个数            //3.解压缩            FILE* fIn = fopen(UnCompressFileName.c_str(), "wb");            assert(fIn);            //获取8位后和1000 0000 按位与            unsigned char value = fgetc(fInput);     //每次读取一个字节,一个字节八个位            HuffmanTreeNode<FileInfo>*cur = root;            while (!feof(fIn))            {                for (int tmp = 7; tmp >= 0; --tmp) //获取压缩文件存放的二进制编码                {                    if (value &(1 << tmp)) //逐个位判断为1或0,从而决定叶节点寻找的方向                        cur = cur->_right;                    else                        cur = cur->_left;                    if (cur->_left == NULL && cur->_right == NULL)                    {                        fputc(cur->_w._ch, fIn);                        cur = root;                        if (--charCount == 0)//有效字符已经读完                        {                            goto end;                        }                    }                }                value = fgetc(fInput);            }        end:            fclose(fIn);            fclose(fInput);            return UnCompressFileName;        }        void HuffmanStrCode(HuffmanTreeNode<FileInfo>*head)        {            if (head)            {                HuffmanStrCode(head->_left);                HuffmanStrCode(head->_right);                if (head->_left == NULL && head->_right == NULL) //找到叶子结点                {                    //从叶子结点到根结点遍历                     HuffmanTreeNode<FileInfo> *cur = head;                    HuffmanTreeNode<FileInfo> *parent = head->_parent;                    string& code = _fileInfo[head->_w._ch]._strCode;                    while (parent)                    {                        if (parent->_left == cur)                            code.push_back('0');                        else                            code.push_back('1');                        cur = parent;                        parent = parent->_parent;                    }                    reverse(code.begin(), code.end());                }            }        }        void getFiles(string path, vector<string>& files)        {            //文件句柄              long hFile = 0;            //文件信息              struct _finddata_t fileinfo;//该结构体是用来存储文件的各种信息            string p;            //string &assign(const char *s);用c类型字符串s赋值,append 指追加            if ((hFile = _findfirst(p.assign(path).append("\\*").c_str(), &fileinfo)) != -1)            {                do                {                    //如果是目录,迭代之                      //如果不是,加入列表                      if ((fileinfo.attrib &  _A_SUBDIR))//判断当前文件夹是否为一个文件夹                    {                        if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)//不为空就向里递归                            getFiles(p.assign(path).append("\\").append(fileinfo.name), files);                    }                    else                    {                        files.push_back(p.assign(path).append("\\").append(fileinfo.name));//                    }                }while (_findnext(hFile, &fileinfo) == 0);//查找成功                _findclose(hFile);            }        }    private:        FileInfo _fileInfo[256];    };

MyHufmanTree.h

#pragma once#include"MyHeap.h" template <class W>struct HuffmanTreeNode{    HuffmanTreeNode<W>* _left;    HuffmanTreeNode<W>* _right;    HuffmanTreeNode<W>* _parent;    W _w;    HuffmanTreeNode<W>(const W&w)        : _left(NULL)        , _right(NULL)        , _parent(NULL)        , _w(w)    {}};template<class W>class HuffmanTree{    typedef HuffmanTreeNode<W> Node;public:    HuffmanTree()        :_root(NULL)    {}    ~HuffmanTree()    {        Destory(_root);    }    HuffmanTree(W*a, size_t n, const W&invalid)//定义非法值,遇到不创建    {        struct Compare        {            bool operator()(const Node*left, const Node* right)            {                return left->_w > right->_w;            }        };        Heap<Node*> minHeap;        for (size_t i = 0; i < n; ++i)        {            if (a[i] != invalid)            {                minHeap.Push(new Node(a[i]));            }        }        while (minHeap.Size() > 1)        {            //取两个权值最小的节点            Node* left = minHeap.Top();            minHeap.Pop();            Node*right = minHeap.Top();            minHeap.Pop();            Node*parent = new Node(left->_w + right->_w);            parent->_left = left;            parent->_right = right;            left->_parent = parent;            right->_parent = parent;            minHeap.Push(parent);        }        _root = minHeap.Top();    }    void Destory(Node* head)    {        if (head)        {            Destory(head->_left);            Destory(head->_right);            delete head;            head = NULL;        }    }    void Printf()    {        _printf(_root);        cout << endl;    }    Node* Top()    {        return _root;    }private:    Node* _root;};

test.cpp

#include "MyHeap.h"#include "MyHufmanTree.h"#include "FileCompress.h"void testCompress(){    HuffCompressFile hf;    string str;    str = hf.Compressfile("1");    string out;    out = hf._UnCompressfile("1.huf");} int main(){    testCompress();    system("pause");    return 0;}