文件压缩

3 篇文章 0 订阅
1 篇文章 0 订阅

本篇文章主要包含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;
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值