开源项目:文件压缩(huffman树版)

原理:

将每个字符转换成对应的编码,而编码都是由0和1组成,那么就可以用位来表示编码对应的字符,从而实现文件变小,即压缩;同样,再根据压缩的步骤反推,即可实现解压缩

思路:

1.统计文件中各个字符出现的次数

定义一个哈希表,使用直接定址法,统计各个字符出现的次数;
要注意的是:字符的大小是-127~128,而哈希表的范围是0~255,所以使用直接定址法的时候强转成无符号字符型,如:++hashtable[(unsigned char)ch];

2.构建huffman树(贪心算法)

①先将哈希表中内容放入小堆中;
②再从堆中取两个最小的值进行相加;
③再将相加的结果插入堆中,并保持仍然是小堆
可以使用优先级队列priority_queue实现

3.生成huffman编码

中序遍历huffman树,多说无益,直接看图和代码
这里写图片描述

    //struct CharInfo
    //{
    //    char _ch;         //字符
    //    LongType _count;  //字符出现的次数
    //    string _code;     //字符对应的huffman编码
    //}
    void GenerateHuffmanCode(Node* root)
    {
        if (root == NULL)
            return;
        if (root->_left == NULL&&root->_right == NULL)
        {
            _hashInfos[(unsigned char)root->_w._ch]._code = root->_w._code;
            return;
        }
        if (root->_left != NULL)
        {
            root->_left->_w._code = root->_w._code + '0';
            GenerateHuffmanCode(root->_left);
        }
        if (root->_right != NULL)
        {
            root->_right->_w._code = root->_w._code + '1';
            GenerateHuffmanCode(root->_right);
        }
    }

4.压缩

将每个字符对应的huffman编码换成比特位,写入压缩文件

5.解压缩

再将压缩文件中的比特位一一读取,寻找huffman树中对应的字符,写入解压缩后的文件

思维导图:

这里写图片描述
这里写图片描述

详细步骤:

压缩过程
①使用ifstream函数读取文件Input.txt文件,统计字符出现次数
②将字符信息:字符_ch、字符出现次数_count、字符的huffman编码_code,存入结构体CharInfo
③将字符对应的CharInfo按_ch直接定址,放入哈希表_hashInfo中
④使用priority_queue创建huffman树,再遍历huffman树,将huffman编码写入哈希表中对应的CharInfo的_code里
⑤将哈希表中CharInfo的_count>0的元素使用ofstream函数写入文件Input.txt.huffman中,最后再加一个_count=0的CharInfo作为分界线
⑥再将每个字符的huffman编码按照“位”,使用ofstream函数写入文件Input.txt.huffman中,压缩完毕
解压过程
⑦使用ofstream函数读取文件Input.txt.huffman中分界线前部分,即哈希表的内容
⑧根据上步创建的哈希表内容,再使用priority_queue创建huffman树
⑨根据哈希表中CharInfoL的_code遍历huffman树,找到对应的字符,使用ofstream函数写入文件Input.txt.unhuffman中,解压完毕

错误手册

这里列举出各种可能导致错误的细节点,供读者查阅,看是否能对上自己的错误原因

①使用ifstrem和ofsteam函数对文本进行输出输入操作时,最好写成以二进制方式,否则可能会出现读取到特殊符号而终止,导致解压缩不完全,二进制方式如下:
ifstream ifs(filename,ios::in|ios::binary);
ofstream ofs(filename,ios::out|ios::binary);
详细的ifstream和ofstream操作可自行搜索
②对字符进行直接定址确定自己在哈希表中的位置时,要注意使用(unsigend char)ch强转,因为哈希表的定义范围是0到255,而字符的大小是-127到128
创建huffman树时,因为节点中保存的是一个结构体而不是一个简单的内置类型,因此在对节点进行“比较”操作的时候需要自己重载这些比较操作符,如:
bool operator>(const T& t);bool operator!=(const T& t);

代码:

功能函数预览

HuffmanTree.h
HuffmanTree(W* w, size_t n, const W& invalid);//构造函数,创建huffman树
~HuffmanTree();//析构,调用Destroy
void Destroy(Node* root);//释放空间
Node* GetRoot();//返回huffman树的根节点

FileCompress.h
FileCompress();//构造函数,初始化哈希表
void GetCharCount(ifstream& ifs,const char*file);//统计文件中字符出现的次数
bool IsExist(const char* file);//判断文件是否存在
string CompressForm(const char* file, char* form);//压缩前的准备和文件检验
string UncompressForm(const char* file, char* form);//解压缩前的准备和文件检验
void GenerateHuffmanCode(Node* root);//获取huffman编码
void Compress(const char* file);//压缩过程,调用上面的函数
void UnCompress(const char* file);//解压过程,调用上面的函数

全部代码

HuffmanTree.h

#ifndef __HUFFMAN_H__
#define __HUFFMAN_H__
#include <iostream>
using namespace std;
#include <queue>
#include <vector>

template <class W>
struct HuffmanTreeNode
{
    HuffmanTreeNode<W>* _left;
    HuffmanTreeNode<W>* _right;
    W _w;
    HuffmanTreeNode(const W& w)
        :_w(w)
        , _left(NULL)
        , _right(NULL)
    {}
};

template <class W>
class HuffmanTree
{
    typedef HuffmanTreeNode<W> Node;
public:
    HuffmanTree()
        :_root(NULL)
    {}

    struct NodeComp
    {
        //仿函数,使之按照结点内的权值比较大小,而不是按指针
        //这种只比较,不修改的写成const最好
        //但是随之而来的问题就是,如果w是自定义类型,而不仅仅是内置类型
        //那么就要注意,const修饰的l和r能不能调用w内部的函数
        //即const对象不能调用非const函数
        bool operator()(const Node* l, const Node* r)
        {
            return l->_w > r->_w;
        }
    };

    HuffmanTree(W* w, size_t n, const W& invalid)//判断非法值,即_count>1才插入,节省空间
    {
        //构建huffman树
        priority_queue<Node*, vector<Node*>, NodeComp> minHeap;
        for (size_t i = 0; i < n; ++i)
        {
            if (w[i] != invalid)
                minHeap.push(new Node(w[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;
            minHeap.push(parent);
        }
        _root = minHeap.top();

    }

    ~HuffmanTree()
    {
        //释放树的空间
        Destroy(_root);
        _root = NULL;
    }
    void Destroy(Node* root)
    {
        if (root == NULL)
            return;
        Destroy(root->_left);
        Destroy(root->_right);
        delete(root);
    }
    Node* GetRoot()
    {
        return _root;
    }
protected:
    Node* _root;
private:
    //防拷贝,C++11的话可以直接在构造函数后面加delete
    HuffmanTree(const HuffmanTree<W>& h);
    HuffmanTree<W>& operator=(const HuffmanTree<W>& h);
};

#endif //__HUFFMAN_H__

FileCompress.h

#ifndef __FILECOMPRESS_H__
#define __FILECOMPRESS_H__
#include "HuffmanTree.h"
#include <fstream>
#include <assert.h>
#include <time.h>
typedef long long LongType;

#define FORM_LENGTH 4   //常见的文件格式长度,如*.txt,扩展名长度为4
#define COMPRESS_FORM ".huffman"    //压缩文件格式

struct CharInfo
{
    char _ch;           //字符
    LongType _count;    //字符出现的次数
    string _code;       //字符对应的huffman编码

    CharInfo operator+(const CharInfo& info)//重载 +
    {
        CharInfo tmp;
        tmp._count = _count + info._count;
        return tmp;
    }
    bool operator>(const CharInfo& info) const//重载 >
    {
        return _count > info._count;
    }
    bool operator!=(const CharInfo& info) const//重载 !=
    {
        return _count != info._count;
    }
};

class FileCompress
{
    typedef HuffmanTreeNode<CharInfo> Node;

public:
    //专门用于在压缩文件里保存字符出现次数的结构体
    //没必要使用CharInfo,因为有string code这一项的浪费开销
    struct ConfigInfo 
    {
        char _ch;
        LongType _count;
    };


    FileCompress()//初始化哈希表
    {
        for (size_t i = 0; i < 256; ++i)
        {
            _hashInfos[i]._ch = i;
            _hashInfos[i]._count = 0;
        }
    }

    //统计文件中字符出现的次数
    void GetCharCount(ifstream& ifs,const char*file)
    {
        char ch;
        while (ifs.get(ch))
        {
            ++_hashInfos[(unsigned char)ch]._count;//这里的ch必须转换成无符号的,因为_hashInfos的下标是从0到255
        }
    }

    bool IsExist(const char* file)//判断文件是否存在
    {
        ifstream ifs(file);
        if (!ifs)
        {
            return false;//不存在
        }
        return true;//存在
    }

    string CompressForm(const char* file, char* form)//压缩前的准备和检验
    {
        string compressfile = file;
        size_t index = compressfile.rfind('.');//从后往前找到文件名中的“.”
        assert(index != string::npos);
        strcpy(form, file + index);//保存源文件格式
        compressfile.erase(index);//删除后缀
        index = compressfile.rfind('\\');//从后往前找到文件名中的第一个“\”
        ++index;
        string name;
        for (size_t i = index; file[i] != '.';++i)//保存源文件名
        {
            name.push_back(file[i]);
        }
        compressfile.erase(index);//删除源文件名


        printf("是否压缩到指定文件?\n");
        printf("1.是\t\t\t2.否\n");

        int select = 0;
        while (1)
        {
            scanf("%d", &select);
            if (select == 1)
            {
                char newRoad[100] = { 0 };
                printf("请输入指定目录:(如:D:\\Game\\ “\\”不能漏写)\n");
                scanf("%s", newRoad);
                string road(newRoad);
                if (IsExist((road + name + COMPRESS_FORM).c_str()))
                {
                    printf("文件已存在,请选择:\n");
                    printf("1.替换\t\t\t2.重命名\n");
                    scanf("%d", &select);
                    if (select == 2)
                    {
                        char newName[100] = { 0 };
                        scanf("%s", newName);
                        road += newName;
                    }
                    else
                    {
                        road += name;
                    }
                }
                else
                {
                    road += name;
                }
                road += COMPRESS_FORM;
                return road;
            }
            else if (select == 2)
            {
                if (IsExist((compressfile + name + COMPRESS_FORM).c_str()))
                {
                    printf("文件已存在,请选择:\n");
                    printf("1.替换\t\t\t2.重命名\n");
                    scanf("%d", &select);
                    if (select == 2)
                    {
                        char newName[100] = { 0 };
                        scanf("%s", newName);
                        compressfile += newName;
                    }
                    else
                    {
                        compressfile += name;
                    }
                }
                else
                {
                    compressfile += name;
                }
                compressfile += COMPRESS_FORM;//替换成指定的压缩文件格式
                return compressfile;
            }
            else
            {
                printf("输入有误,重新选择:\n");
            }
        }
    }

    string UncompressForm(const char* file, char* form)//解压缩前的准备和检验
    {
        string compressfile = file;
        size_t index = compressfile.rfind('.');//从后往前找到文件名中的“.”
        assert(index != string::npos);
        compressfile.erase(index);//删除后缀
        index = compressfile.rfind('\\');//从后往前找到文件名中的第一个“\”
        ++index;
        string name;
        for (size_t i = index; file[i] != '.'; ++i)//保存源文件名
        {
            name.push_back(file[i]);
        }
        compressfile.erase(index);//删除压缩文件名

        printf("是否解压到指定文件夹?\n");
        printf("1.是\t\t\t2.否\n");

        int select = 0;
        while (1)
        {
            scanf("%d", &select);
            if (select == 1)
            {
                char str[100] = { 0 };
                printf("请输入指定目录:(如:D:\\Game\\ “\\”不能漏写)\n");
                scanf("%s", str);
                string road(str);
                if (IsExist((road + name + form).c_str()))
                {
                    printf("文件已存在,请选择:\n");
                    printf("1.替换\t\t\t2.重命名\n");
                    scanf("%d", &select);
                    if (select == 2)
                    {
                        char newName[100] = { 0 };
                        printf("请输入新的名字:\n");
                        scanf("%s", newName);
                        road += newName;
                    }
                    else
                    {
                        road += name;
                    }
                }
                else
                {
                    road += name;
                }
                road += form;
                return road;
            }
            else if (select == 2)
            {
                if (IsExist((compressfile + name + form).c_str()))
                {
                    printf("文件已存在,请选择:\n");
                    printf("1.替换\t\t\t2.重命名\n");
                    scanf("%d", &select);
                    if (select == 2)
                    {
                        char newName[100] = { 0 };
                        printf("请输入新的名字:\n");
                        scanf("%s", newName);
                        compressfile += newName;
                    }
                    else
                    {
                        compressfile += name;
                    }
                }
                else
                {
                    compressfile += name;
                }
                compressfile += form;//替换成指定的压缩文件格式
                return compressfile;
            }
            else
            {
                printf("输入有误,重新选择:\n");
            }
        }
    }

    void GenerateHuffmanCode(Node* root)//获取huffman编码
    {
        if (root == NULL)
            return;
        if (root->_left == NULL&&root->_right == NULL)
        {
            _hashInfos[(unsigned char)root->_w._ch]._code = root->_w._code;
            return;
        }
        if (root->_left != NULL)
        {
            root->_left->_w._code = root->_w._code + '0';
            GenerateHuffmanCode(root->_left);
        }
        if (root->_right != NULL)
        {
            root->_right->_w._code = root->_w._code + '1';
            GenerateHuffmanCode(root->_right);
        }
    }

    void Compress(const char* file)
    {
        ifstream ifs(file, ios::in | ios::binary);//0.打开源文件

        GetCharCount(ifs,file);//1.统计文件中字符出现的次数

        CharInfo invalid;
        invalid._count = 0;
        HuffmanTree<CharInfo> tree(_hashInfos, 256, invalid);//2.生成huffman树

        GenerateHuffmanCode(tree.GetRoot());//3.生成huffman编码

        char form[FORM_LENGTH + 1] = { 0 };
        string compressfile = CompressForm(file, form);//4.压缩前的准备和检验
        ofstream ofs(compressfile.c_str(), ios::out | ios::binary);//创建该文件名的文件,并写入内容

        //计时器
        clock_t start = 0, end = 0;
        start = clock();

        //5.压缩
        //压缩三部分内容:源文件格式+字符出现次数信息+源文件内容信息
        for (size_t i = 0; i < FORM_LENGTH+1; ++i)//5.1将源文件后缀,即格式写入压缩文件,方便解压缩的时候恢复原格式
        {
            ofs.put(form[i]);
        }

        for (size_t i = 0; i < 256; ++i)//5.2将_hashInfos中字符出现次数>0的元素写入压缩文件
        {   
            if (_hashInfos[i]._count>0)
            {
                ConfigInfo info;
                info._ch = _hashInfos[i]._ch;
                info._count = _hashInfos[i]._count;
                ofs.write((const char*)&info, sizeof(ConfigInfo));
            }
        }

        ConfigInfo over;
        over._count = 0;
        ofs.write((const char*)&over, sizeof(ConfigInfo));//5.3设置分界线

        char ch;
        char value = 0;
        int pos = 0;
        ifs.clear();
        ifs.seekg(0);
        while (ifs.get(ch))//5.4将huffman编码写入
        {
            string& code = _hashInfos[(unsigned char)ch]._code;
            for (size_t i = 0; i < code.size(); ++i)
            {
                if (code[i] == '0')
                    value &= (~(1 << pos));
                else if (code[i] == '1')
                    value |= (1 << pos);
                else
                    assert(false);
                ++pos;
                if (pos == 8)
                {
                    ofs.put(value);
                    pos = 0;
                    value = 0;
                }
            }

        }
        if (pos > 0)
        {
            ofs.put(value);
        }
        end = clock();
        printf("压缩用时:%d ms\n", end - start);
    }

    void UnCompress(const char* file)
    {
        ifstream ifs(file, ios::in | ios::binary);//0.打开压缩文件

        //2.读取三部分信息:源文件格式+字符出现次数信息+源文件内容信息

        char form[FORM_LENGTH + 1];
        for (size_t i = 0; i < FORM_LENGTH + 1; ++i)//2.读取源文件格式信息
        {
            ifs.get(form[i]);
        }

        string uncompressfile = UncompressForm(file,form);//3.解压缩前的准备和检验
        ofstream ofs(uncompressfile.c_str(), ios::out | ios::binary);//创建该文件名的文件,并写入内容

        while (1)//4.读取字符串出现次数信息
        {
            ConfigInfo info;
            ifs.read((char*)&info, sizeof(ConfigInfo));
            if (info._count > 0)
            {
                _hashInfos[(unsigned char)info._ch]._count = info._count;
            }
            else
            {
                break;
            }
        }

        //计时器
        clock_t start = 0, end = 0;
        start = clock();

        //5.重建huffman树
        CharInfo invalid;
        invalid._count = 0;
        HuffmanTree<CharInfo> tree(_hashInfos, 256, invalid);

        //6.解压缩
        //根据读取的huffman编码在huffman树中找到对应字符,写入到文件中
        Node* root = tree.GetRoot();
        LongType filecount = root->_w._count;
        Node* cur = root;
        char ch;
        while (ifs.get(ch))
        {
            for (size_t i = 0; i < 8; ++i)
            {
                if (ch&(1 << i))//1
                    cur = cur->_right;
                else//0
                    cur = cur->_left;
                if (cur->_left == NULL&&cur->_right == NULL)
                {
                    ofs.put(cur->_w._ch);
                    cur = root;
                    if (--filecount == 0)
                    {
                        break;
                    }
                }
            }
        }
        end = clock();
        printf("解压用时:%d ms\n", end - start);
    }
private:
    CharInfo _hashInfos[256];
};

void TestCompress(const char* filename)
{
    FileCompress fc;
    fc.Compress(filename);
}

void TestUnCompress(const char* filename)
{
    FileCompress fc;
    fc.UnCompress(filename);
}

#endif //__FILECOMPRESS_H__

test.c

#include "FileCompress.h"

void menu()
{
    printf("****************** 欢迎使用huffman文件压缩 ******************\n");
    printf("\n");
    printf("****************** 1.压缩文件   2.解压文件 ******************\n");
    int select = 0;
    scanf("%d", &select);
    switch (select)
    {
    case 1:
    {
              printf("请输入文件所在路径和文件格式:(如:D:\\test\\filename.txt)\n");
              char filename[100] = { 0 };
              scanf("%s", filename);
              TestCompress(filename);
    }
        break;
    case 2:
    {
              printf("请输入文件所在路径和文件格式:(如:D:\\test\\filename.huffman)\n");
              char filename[100] = { 0 };
              scanf("%s", filename);
              TestUnCompress(filename);
    }
        break;
    default:
        break;
    }
}

int main()
{
    menu();
    system("pause");
    return 0;
}

压缩率

文件类型 源文件大小 压缩大小 压缩率
视频文件 78.8MB 78.7MB 0.99
word文档 101K 105K 1.04
长篇中文小说txt 8.56MB 6.25MB 0.73

总结:huffman树压缩适用于字符出现次数差值较大,分布不平均的文件

猜你喜欢

转载自blog.csdn.net/w_y_x_y/article/details/80893418
今日推荐