基于哈夫曼编码的C++压缩程序

 

 

 


前言

本文由一个数据结构大作业改造而来,记录制作过程和部分制作心得


文章已修改,使用bitset代替部分位运算

 

源代码下载地址:https://download.csdn.net/download/qq_33911878/19542719

 

 

目录

 

前言

一、初步确定压缩和解压过程

压缩过程

1.读取文件

2.一个字符一个字符读取,根据读取到的字符建立字符出现频率表

3.建立哈夫曼树

4.这里使用优先队列,每次选择两个权值最小字符作为结点建立树,并再次将得到的树的根结点放入优先队列中, 最终创建哈夫曼树

需要前置知识:STL优先队列使用,及其自定义排序方法

5.遍历哈夫曼树,根据叶子结点路径,建立关于每个字符对应到哈夫曼编码的映射,并储存于哈希表中

6、创建压缩文件,并写入前置信息

7.再次读取原文件,根据每个字符转换到对应哈夫曼编码输出到新压缩文件中。

完成压缩过程

解压过程

1.读取文件后缀并创建新文件,新文件名称可根据用户输入创建。

2.读取还原哈夫曼树所需信息,既字符出现频率表。

3.根据字符出现频率表重新还原哈夫曼树

4.开始读取压缩文件中哈夫曼编码数据,将哈夫曼编码对应字符以二进制形式写入解压文件中。

5.读完压缩文件,解压完成。

二、确定大类和内部函数设计,及具体函数内部实现及步骤解释

 

1.大类设计:

第一个大类:HuaffmanTree 在 “HuaffmanTree.h” 头文件中定义

 树结点的定义:

优先队列自定义排序方法函数:

 

哈夫曼树构造函数:

创建哈夫曼树核心函数:

字符频率哈希表向优先队列转换函数:

哈夫曼树清空函数:用于回收哈夫曼树空间,哈夫曼树析构函数调用

第二大类:PackPress定义在PackPress.h头文件中,用于压缩文件

哈夫曼路径储存结构:

压缩构造函数:

 

压缩析构函数:

压缩综合函数:

 

原文件读取函数:

 

创建哈夫曼树函数:

遍历哈夫曼树函数:

 

压缩文件写入函数(核心):

 

 

 

第三大类:PackRelease定义在PackRelease.h头文件中用于解压缩。

 

构造函数:

 

析构函数:

 

解压综合函数:

                                                                          

解压文件写入函数:

还原哈夫曼树函数:

获得解压文件名称后缀函数:

第四大类:Press定义在Press.h头文件中,用于和用户交互,同时处理文件名称,文件保存路径等问题的预处理类。

用户接口函数:

三、文件压缩和解压过程函数流程及函数调用顺序

压缩过程:

——Press构造函数

——Start函数

——GetSuffix(FileName)函数

——PackPress构造函数

——StartPress()函数

——(ReadFiles(weight)函数-CreateTree(weight)函数)

——(WriteFiles(weight)函数-TransLookTree()函数)

——完成压缩

解压缩过程:

——Press构造函数

——Start函数

——PackRelease 构造函数

——StartRelease()函数

——GetSuffixName(fp)函数

——ReCreateTree(fp)函数

——ReleaseFiles(fp)函数

——完成解压过程

 

程序测试

该程序压缩和解压缩时间与现有压缩软件相比较慢,最主要占用时间部分是读文件然后一个字符一个字符的进行操作,该过程压缩时重复两次,解压时重复一次,占用压缩和解压缩文件90%以上的时间.

总结


 

一、初步确定压缩和解压过程

压缩过程

1.读取文件

 需要前置知识包括

 文件操作问题C++文件操作大全(过程需要使用最基本的文件操作)

二进制读取写入问题二进制文件操作,位运算(需要灵活操作二进制每一位数字,如批量获取指定范围二进制位,与批量修改二进制位)

了解数据储存的本质   计算机中所有类型文件都以二进制形式储存于计算机或者说硬盘中,理论上只要还原所有二进制位即可还原该文件。

举例来说:当我们使用文本文件打开任何不是文本类型的文件时都会出现乱码,其实这就是二进制位以字符形式表现出来的样子。C++中每个字符占8个二进制位。比如我们用文本打开一个MP3格式文件时文本中出现的每一个字符乱码就是每8个二进制位在文本形式下以字符格式表现,所有理论上我们之需要还原这些字符乱码那么我们便可还原该MP3文件,而这些乱码本身又是二进制,最终我们只要还原每个二进制位是0或1,即可还原该文件。

这也就是可以压缩任何格式文件的原理。

文件读取速度:  文件读取速度使用不同函数会又不同的速度,个人使用时感觉上C语言文件操作会稍微快于C++文件操作函数。这个涉及最后压缩时间优化问题,后文会提。

文件操作函数测试:部分函数需要根据需求自行测试后在使用,否则容易导致Bug,比如feof()函数

while (!feof(ifs))
{
   fread((char*)data,1,sizeof(data),ifs);
}

在这样使用时,while语句会在到达文件末尾时再进入一次循环。具体照成的原因如下:

注意:feof判断文件结束是通过读取函数fread/fscanf等返回错误来识别的,故而判断文件是否结束应该是在读取函数之后进行判断。比如,在while循环读取一个文件时,如果是在读取函数之前进行判断,则如果文件最后一行是空白行,可能会造成内存错误。

 

2.一个字符一个字符读取,根据读取到的字符建立字符出现频率表


3.建立哈夫曼树

    需要前置知识:哈夫曼树建立过程

4.这里使用优先队列,每次选择两个权值最小字符作为结点建立树,并再次将得到的树的根结点放入优先队列中, 最终创建哈夫曼树

struct cmp
{
	bool operator()(pair<unsigned long long, TreeNode*>& r, pair<unsigned long long, TreeNode*>& s) {
		if (r.first == s.first)
		{
			return r.second->val > s.second->val;
		}
		return r.first > s.first;
	}
};

priority_queue<pair<unsigned long long, TreeNode*>, vector<pair<unsigned long long, TreeNode*>>, cmp> qNode

需要前置知识:STL优先队列使用,及其自定义排序方法


5.遍历哈夫曼树,根据叶子结点路径,建立关于每个字符对应到哈夫曼编码的映射,并储存于哈希表中
 

需要前置知识:树的遍历,STL哈希表的使用

涉及问题:

哈夫曼编码储存问题(应该使用何种数据类型储存哈夫曼编码)(该问题涉及到文件压缩时写入新压缩文件时的速度问题)

 

6、创建压缩文件,并写入前置信息

真正写入原文件哈夫曼编码之前,我们需要写入该文件部分信息。方便解压时使用。

涉及问题:

哈夫曼树写入形式:我们解压时需要对照哈夫曼编码,还原原文件对应字符,也就是原文件二进制。我们压缩时写入的只是这些字符对应的哈夫曼编码,所以我们需要根据原哈夫曼树来确定这些哈夫曼编码表示的字符是谁。所以我们在解压时需要压缩时建立的哈夫曼树来进行解压或者说解密。

在C++中所以数据类型都有自己所占空间字节数,使用sizeof()即可得到,我们自定义类也同样有大小,也就意味这这些数据类型其实也是以二进制形式存在的,只有我们得到这些数据的二进制信息,要这些信息赋值给一个新的该种类型,我们即可得到原本该类型的数据。

例如:int类型大小4个字节,赋值为3,将其以二进制形式写入文件中。以二进制形式读取该文件,并读取4个字节大小的二进制位,赋值给一个新的int类型,这个int的数值就变成了3.

		int sufSize=0;
		fread(&sufSize, sizeof(sufSize), 1, fp);

注意:我们不可以使用该方法储存或读取带有指针的类型。这样只会将该类型的指针写入或者读取出来。而指针所指向的内容并不会写入文件中。所以在读取时可能导致类型中的指针指向一个没有被分配空间的地方,形成危险的野指针。

因此我们不能直接将链式建立的哈夫曼树写入文件中,但是我们可以写入建立哈夫曼树所需信息。不带指针即可。建立哈夫曼树需要的是字符出现频率表。因此我们只需要将每个字符与其对应出现频率做成一个键值对,也就是pair类型,再一个一个写入即可。在解压时一个一个读取,重新建立还原压缩时的哈夫曼树即可。

因此我们需要写入频率表数据个数,方便解压时使用,才能在解压时知道要读取多少个

解压文件类型写入:我们解压文件时需要确定文件是什么类型,所以我们需要将原文件类型,如MP3后缀信息写入。注意不能直接将string类型的二进制信息写入,因为string类型带有指针。我们需要将后缀名一个字符一个字符的写入。同时也需要写入字符个数,解压时才知道需要读取多少个字符作为文件后缀

		//字符出现频率哈希表
        unordered_map<unsigned char, unsigned long long>& weight
       	//写入预置信息
		//写入解压文件所需文件名后缀
		int sufSize = suffixName.size();                 //写入后缀字符个数
		fwrite((char*)&sufSize, sizeof(sufSize), 1, ofs);//写入后缀字符个数
		for (auto& c : suffixName)
		{
			fwrite(&c, sizeof(c), 1, ofs);
		
        //储存构建哈夫曼树所需内容大小
		int weightSize = weight.size();
		fwrite((char*)&weightSize, sizeof(weightSize),1,ofs);
		//储存构建哈夫曼树所需,字符出现权值的哈希表
		for (auto &c:weight)
		{
			fwrite((char*)&c, sizeof(c),1,ofs);
		}
        //预置信息写入完成

 

7.再次读取原文件,根据每个字符转换到对应哈夫曼编码输出到新压缩文件中。

需要前置知识:

文件写入操作:

位运算:

涉及问题:

哈夫曼编码储存问题:由于文件操作最小单位长度是一个字节,也就是8个二进制位,然而哈夫曼编码长度不确定最短:1位二进制,最长位:128位二进制。我们不能8个二进制只储存一个字符对应的哈夫曼编码,这样等于没有压缩。所以我们需要将不同长度哈夫曼编码合并。

比如:a,b,c所对应哈夫曼编码分别为:010,011,01。由于最文件操作以8个二进制位为单位,所以我们需要将a,b,c对应哈夫曼编码写入一个字符中。合并成01001101,并将其赋值给一个字符,在将该字符以二进制形式写入文件中。

写入速度问题:使用如何写入文件文件会更快。

完成压缩过程
 


解压过程

1.读取文件后缀并创建新文件,新文件名称可根据用户输入创建。

2.读取还原哈夫曼树所需信息,既字符出现频率表。

 首先读取频率表中数据个数,使用int类型储存该数值,根据该数值确定循环次数逐个读取出字符及其对应频率的数据,并将数据放入哈希表中。

3.根据字符出现频率表重新还原哈夫曼树


4.开始读取压缩文件中哈夫曼编码数据,将哈夫曼编码对应字符以二进制形式写入解压文件中。

使用位运算读取二进制位,并且按0,1走哈夫曼树,0表示哈夫曼树左儿子,1表示哈夫曼树右儿子。

到达树的叶子结点时,输出该字符到新文件中。
 

5.读完压缩文件,解压完成。
 


二、确定大类和内部函数设计,及具体函数内部实现及步骤解释

1.大类设计:

这里设计四个类,分别在四个头文件中。

第一个大类:HuaffmanTree 在 “HuaffmanTree.h” 头文件中定义,大部分头文件在该类中引用

#pragma once
#include<fstream>
#include<unordered_map>
#include<queue>
#include<map>
#include<vector>
#include<bitset>
using namespace std;

哈夫曼树结点定义
struct TreeNode{};

优先队列自定义排序比较仿函数
struct cmp{};

哈夫曼树类
class HuaffmanTree
{
public:
    哈夫曼树根结点
	TreeNode* root;

    哈夫曼树构造函数传入,字符对应出现频率哈希表,调用创建哈夫曼函数核心函数,由此建立哈夫曼树。
    HuaffmanTree(unordered_map<unsigned char, unsigned long long> weight){}
    
    哈夫曼树析构函数调用ClearTree函数递归回收释放内存
	~HuaffmanTree() { ClearTree(root);}

private:
    创建哈夫曼树核心函数,构造函数会调用该函数传入需要参数,返回创建好的哈夫曼树根结点
	TreeNode* CreateTree(unordered_map<unsigned char, unsigned long long>& weight){}
    
    回收并释放哈夫曼树分配的内存空间,析构函数会调用此函数
    void ClearTree(TreeNode* root){}

由该函数将字符出现频率哈希表,转换为优先队列,用于创建哈夫曼树
void WeightSort(priority_queue<pair<unsigned long long, TreeNode*>, vector<pair<unsigned long long, TreeNode*>>, cmp> &qNode, unordered_map<unsigned char, unsigned long long>& weight){}




 树结点的定义:

struct TreeNode {
    当该结点作为叶子结点时,此处储存对应源文件中8个二进制位所表示的字符
	unsigned char val = 0;

    表示该结点以及其子结点权值总和
	unsigned long long weight = 0;

    左儿子
	TreeNode* left;

    右儿子
	TreeNode* right;

    树结点多个构造函数重载,方便以各种方式创建结点
    默认构造函数
	TreeNode() : val(0), left(nullptr), right(nullptr) {}

    传入字符构造函数
	TreeNode(unsigned char x) : val(x), left(nullptr), right(nullptr) {}

    传入权值构造函数
	TreeNode(unsigned long long y) :  weight(y),left(nullptr), right(nullptr) {}

    传入权值和字符构造函数
	TreeNode(unsigned long long y, unsigned char x) : val(x), weight(y), left(nullptr), right(nullptr) {}

    传入字符,左右儿子构造函数
	TreeNode(unsigned char x, TreeNode* left, TreeNode* right) : val(x), left(left), right(right) {}

    传入权值,左右儿子构造函数
	TreeNode(unsigned long long y, TreeNode* left, TreeNode* right) : weight(y), left(left), right(right) {}

    传入权值,字符,左右儿子构造函数
	TreeNode(unsigned long long y, unsigned char x, TreeNode* left, TreeNode* right) : val(x), weight(y), left(left), right(right) {}

};

优先队列自定义排序方法函数:

struct cmp
{
    重载()运算符,将该类作为仿函数使用
	bool operator()(pair<unsigned long long, TreeNode*>& r, pair<unsigned long long, TreeNode*>& s) {
        建立哈夫曼树时有可能多个结点权值相等,此时返回字符比较结果
		if (r.first == s.first)
		{
			return r.second->val > s.second->val;
		}
        当权值不同时返回权值比较结果
		return r.first > s.first;
	}
};

 

哈夫曼树构造函数:

    需要传入以字符为键,字符出现频率为值的哈希表
    HuaffmanTree(unordered_map<unsigned char, unsigned long long> weight)
	{
        调用创建树的核心函数,并将返回的结点作为树的根结点
		root = CreateTree(weight);
	}

创建哈夫曼树核心函数:

	TreeNode* CreateTree(unordered_map<unsigned char, unsigned long long>& weight)
	{
        创建空优先队列
		priority_queue<pair<unsigned long long, TreeNode*>,vector<pair<unsigned long long, TreeNode*>>,cmp> qNode;

        调用权值读取函数,将字符与频率哈希表中数据一个个读入,空优先队列中(字符出现频率既为结点权值)
		WeightSort(qNode, weight);//创建初始优先队列

        哈夫曼树顺序构造法,不断从优先队列中取出最小权值的两个结点,构造一棵新的树,该树根结点
权值为其左右儿子结点权值之和。而其根结点字符数据等于左儿子字符数据(方便优先队列排序)

		while (qNode.size()>1)//当优先队列中数据个数大于一时进行
		{
            取出最小权值结点键值对,赋值给p
			pair<unsigned long long, TreeNode*> p = qNode.top();

            删除最小权值结点
			qNode.pop();

            取出第二小权值结点键值对,赋值给q
			pair<unsigned long long, TreeNode*> q = qNode.top();

            删除第二小权值结点
			qNode.pop();

            创建新结点,并将p,q作为其左右儿子,其权值大小等于p,q权值之和,其字符等于p字符
			TreeNode* node = new TreeNode((p.first + q.first), p.second->val, p.second,q.second);

            将新树根结点与其权值对应,创建键值对pair
			pair<unsigned long long, TreeNode*> s(p.first + q.first, node);

            将创建好的新树根结点放入优先队列中备选
			qNode.push(s);
		}

        最终哈夫曼树创建完成返回哈夫曼树根结点
		return qNode.top().second;
	}

注意:当优先队列中数据权值相等时出队顺序会变成随机,也就是说一棵哈夫曼树中,有两个结点若不是叶子结点,那么其字符数据为空,当其权值相等时,优先队列无法比较两个结点先后顺序,就照成其出队顺序随机,那么压缩时创建的哈夫曼树,和解压时的哈夫曼树构造可能就会不同,就照成解压时哈夫曼编码对应字符与压缩时不一致,最终解压错误变成一堆乱码。所以为了解决该问题,我们需要将不是叶子结点的树结点的字符值设置为其左儿子的字符值,这样优先队列才能比较出确定的顺序。(也是我写该程序中遇到的bug之一)

字符频率哈希表向优先队列转换函数:

	传入权值表,填入优先队列
void WeightSort(priority_queue<pair<unsigned long long, TreeNode*>, 
    vector<pair<unsigned long long, TreeNode*>>, cmp> &qNode, unordered_map<unsigned 
    char, unsigned long long>& weight) 
	{
       循环遍历哈希表,逐个将字符频率键值对创建成一个树结点,并将该结点与字符出现频率对应成新的键值对,放入优先队列中
		for (auto&c:weight)
		{
			TreeNode* p = new TreeNode(c.second,c.first);
			qNode.push(make_pair(c.second, p));
		}
	};

哈夫曼树清空函数:用于回收哈夫曼树空间,哈夫曼树析构函数调用

递归后序遍历树,从叶子结点开始回收空间
    void ClearTree(TreeNode* root)
	{
		if (!root)
		{
			return;
		}
		ClearTree(root->left);
		ClearTree(root->right);
		delete(root);
	}

第二大类:PackPress定义在PackPress.h头文件中,用于压缩文件

#pragma once
#include"HuaffmanTree.h"
using namespace::std;

class PackPress
{
public:
    构造函数,会调用StartPress函数完成压缩
	PackPress(string &p,string &e, string &s)//传入需要压缩的文件名及路径{};
    
    析构函数,会回收指向哈夫曼树的指针所指向的空间,
这时会调用哈夫曼树类的析构函数,该析构函数会调用清空哈夫曼树的函数,最终完成回收空间。
	~PackPress() { if(huaffTree)delete (huaffTree); };

   接口函数,压缩综合区,调用各个函数组合完成压缩
	int StartPress(){}
private:
    用于记录需要压缩文件的名称以及路径
	string pressFileName;

    用于记录压缩后文件名称
	string emitFileName;

    用于记录原文件后缀名,写入压缩文件,方便解压时读取
	string suffixName;

    一个指向哈夫曼树类的指针
	HuaffmanTree* huaffTree;//哈夫曼树

用于读取原文件,创建字符对频率哈希表,并调用创建哈夫曼树函数,创建哈夫曼树
bool ReadFiles(unordered_map<unsigned char, unsigned long long> &weight){}

用于创建压缩文件,并根据原文件将原文件中字符所对应哈夫曼编码写入压缩文件中,并写入前置信息
bool WriteFiles(unordered_map<unsigned char, unsigned long long>& weight){}

用于调用哈夫曼树构造函数,传入字符频率表,创建哈夫曼树,并将哈夫曼树指针指向创建的哈夫曼树
TreeNode* CreateTree(unordered_map<unsigned char, unsigned long long>& weight){}

递归遍历哈夫曼树,记录每个叶子结点字符所对应哈夫曼路径,也就是哈夫曼编码,创建哈希表,写入压缩时使用
void TransLookTree(unordered_map<unsigned char, pair<int, bitset<256>>> &map,TreeNode* root,int n, bitset<256> x){}

 

 

压缩构造函数:

	PackPress(string &p,string &e, string &s)传入需要压缩的文件名及路径
	{
		huaffTree = __nullptr;
		pressFileName = p;
		emitFileName = e;
		suffixName = s;
		StartPress();调用综合服务区
	};

 

压缩析构函数:

~PackPress() { if(huaffTree)delete (huaffTree); };

压缩综合函数:

用户接口函数,压缩综合区
int StartPress()
	{
        储存所有字符出现频率
		unordered_map<unsigned char,unsigned long long> weight;
		
        读取数据,哈夫曼树创建成功
        if(ReadFiles(weight)==0)return 0;
 		
        写数据,完成压缩
        if(WriteFiles(weight)==0)return -1;
        return 1;
	}

 

原文件读取函数:

bool ReadFiles(unordered_map<unsigned char, unsigned long long> &weight)//读取文件,以二进制读取,并创建权值表,创建哈夫曼树。
	{
        打开需要压缩的文件
		FILE* ifs;
		fopen_s(&ifs,pressFileName.c_str(),"rb");
		if (ifs == nullptr) return 0;
        
        开一个10240字节大小的数组用于当读取缓存区,避免一个字符一个字符读取,加快读取速度
		unsigned char it[10240] = { 0 };
		while (!feof(ifs))
		{
            用k记录到底读取到了几个字节的数据
			int k=fread(it,1,sizeof(it),ifs);
			int i = 0;
            仅遍历实际读取到的数据
			while (i<k)
			{
                读取到字符后使其出现频率加一
				weight[it[i++]]++;
			}
		}
        关闭文件
		fclose(ifs);
 
        文件读取完成后调用创建哈夫曼树函数,创建哈夫曼树
		CreateTree(weight);//创建哈夫曼树
		return 1;
	}

 

创建哈夫曼树函数:

     传入字符频率哈希表
    TreeNode* CreateTree(unordered_map<unsigned char, unsigned long long>& weight)
	{
       调用哈夫曼树类构造函数,传入字符频率哈希表,创建哈夫曼树,并将指针指向该树
		huaffTree = new HuaffmanTree(weight);
        
        返回创建好的哈夫曼树的根结点
		return huaffTree->root;
	}

遍历哈夫曼树函数:

//传入空哈希表,遍历哈夫曼树,填哈希表
void TransLookTree(unordered_map<unsigned char, pair<int, bitset<256>>> &map,TreeNode* root,int n, bitset<256> x)//传入空哈希表,遍历哈夫曼树,填哈希表
	{
		if (!root->right&&!root->left)
		{
			map.insert(make_pair(root->val,make_pair(n,x)));
			return;
		}
		TransLookTree(map, root->left,n+1,x);
		x[n] = 1;
		TransLookTree(map, root->right,n+1,x);
		return;
	}

 

注意:    此处使用bitset<256>储存哈夫曼编码,bitset是C++自带类型,可以使不会位运算的新手更方便的操作二进制位。具体操作请看bitset 使用方法

原因:若使用数组储存哈夫曼编码会使得,写入哈夫曼编码时只能一位一位的对哈夫曼编码进行操作,这样会大大增加时间复杂度,这里使用bitset可以方便二进制操作,同时可以对二进制位进行批量处理,加快合并写入哈夫曼编码速度。

 

注意:

一、此处哈希表的键是字符,而值是我们自定义的结构体path与哈夫曼编码长度n形成的pair类型。用n作为pair的键,记录哈夫曼实际编码长,这样我们在写入时,才能知道自定义结构体中16个字节,128个二进制位中到底有多少位是哈夫曼编码有实际意义。方便写入哈夫曼编码时使用。

这里不选择数组记录哈夫曼编码的原因,在哈夫曼编码路径储存结构,此处不再赘述。

二、关键语句

x[n] = 1;当往右走时bitset结构中的二进制位置为1

 

压缩文件写入函数(核心):

bool WriteFiles(unordered_map<unsigned char, unsigned long long>& weight)
	{
		创建哈夫曼编码的哈希表,哈夫曼编码表(空)
		unordered_map<unsigned char,pair<int,bitset<256>>> map;  
       
        哈夫曼路径储存处
		bitset<256> x;
        
        遍历哈夫曼树并创建哈夫曼编码表
		TransLookTree(map, huaffTree->root,0,x);     
		
        打开原文件,创建新压缩文件并打开
		FILE* ifs;
		FILE* ofs;
		fopen_s(&ifs, pressFileName.c_str(), "rb");
		fopen_s(&ofs, emitFileName.c_str(), "wb+");
		if (ifs == nullptr)return 0;
		if (ofs == nullptr)return 0;
		
        写入预置信息
		写入解压文件所需文件名后缀
		int sufSize = suffixName.size();
		fwrite((char*)&sufSize, sizeof(sufSize), 1, ofs);
		for (auto& c : suffixName)
		{
			fwrite(&c, sizeof(c), 1, ofs);
		}
		
        储存构建哈夫曼树所需内容大小
		int weightSize = weight.size();
		fwrite((char*)&weightSize, sizeof(weightSize),1,ofs);
		
        储存构建哈夫曼树所需,字符出现权值的哈希表
		预置信息写入完成
		for (auto &c:weight)
		{
			fwrite((char*)&c, sizeof(c),1,ofs);
		}
		

        转换所有原文件字节信息为哈夫曼编码,并储存入压缩文件中
		以256二进制位位单位写入
        以下为合并每个字节的哈夫曼编码到256位二进制数据类型n中,并写入。
		int eight = 256;
		bitset<256> n=0;
		while (!feof(ifs))
		{
			int k=fread(s, 1,sizeof(s),ifs);
			int i = 0;
			while (i < k)
			{
				int j = map[s[i]].first;
				if (j <= eight)
				{
					n|= map[s[i]].second << (256 - eight);
					eight -= j;
				}
				else
				{
					n |= map[s[i]].second << (256 - eight);
					fwrite(&n, sizeof(n), 1, ofs);
					n = 0;
					n |= map[s[i]].second >> eight;
					eight = 256 - (j - eight);
				}
				i++;
			}	
		}

        该句用来处理到,最后哈夫曼编码长度不够64位时,直接写入,可能会导致一些多余信息
        不过不影响原文件
		if(eight!=0) fwrite(&n,(256 - eight)/8+1, 1, ofs);


        哈夫曼编码写入完毕

		关闭文件
		fclose(ifs);
		fclose(ofs);
		return 1;
	}

 

注意:此处合并哈夫曼并写入部分若是使用数组储存哈夫曼编码,只能对哈夫曼编码一位一位的操作,这样时间复杂度将会是文件中所有字符哈夫曼编码长度的总和。

然而使用bitset<256>数据类型,写入时可以使用这个储存结构自带函数对哈夫曼编码批量操作,使速度大大加快。第一版压缩程序中使用动态数组储存哈夫曼编码路径,时间花费极高。

而改用该结构后花费时间仅为原本的10%。具体使用方法请看:bitset操作方法

 

 

第三大类:PackRelease定义在PackRelease.h头文件中用于解压缩。

#pragma once
#include "HuaffmanTree.h"
using namespace::std;

解压缩类
class PackRelease
{
public:
    解压缩构造函数。会调用StartRelease函数进行解压操作
	PackRelease(string p, string r) {};
	~PackRelease() { delete(huaffTree); };
    
    解压缩综合函数会组合调用私有函数成员,完成解压操作。
	int StartRelease(){}

private:
    指针指向还原后的哈夫曼树
	HuaffmanTree* huaffTree;
    压缩文名,包括文件路径
	string pressFileName;
    解压缩后的文件名,包括文件保存路径
	string releaseFileName;

    开始解压缩,创建解压文件,根据压缩文件信息,与还原的哈夫曼树,完成解压
	bool ReleaseFiles(FILE* frp){}

    用于还原哈夫曼树,会一个个读取压缩文件中储存的字符频率键值对,并放入哈希表中,
    调用哈夫曼树类的构造函数还原压缩时的哈夫曼树
	HuaffmanTree* ReCreateTree(FILE* fp){}

    用于获取前置信息,既解压后文件类型,既文件名称后,如.mp3格式
	void GetSuffixName(FILE* fp){}
}

 

构造函数:

	PackRelease(string p, string r) 
	{
        记录压缩文件路径及名称方便其他函数使用
		pressFileName = p;

        记录解压文保存路径及名称方便其他函数使用
		releaseFileName = r;

        调用解压综合函数,完成解压
		StartRelease();
	};

 

析构函数:

	~PackRelease() { delete(huaffTree); };

 

解压综合函数:

	int StartRelease()
	{
        打开压缩文件
		FILE* fp;
		fopen_s(&fp, pressFileName.c_str(), "rb");
		if (fp == nullptr)return 0;

		获取解压文件类型,及文件后缀读入,如.mp3,并将其写入成员数据releaseFileName中
		GetSuffixName(fp);
		后缀写入完成
      
        调用哈夫曼树还原函数,重新还原压缩时建立的哈夫曼树,
        并使成员数据huaffTree指向还原的哈夫曼树
		huaffTree = ReCreateTree(fp);

        调用解压函数,最终完成解压
		ReleaseFiles(fp);

        关闭压缩文件
		if(fp)fclose(fp);
		return 1;
	}

                                                                          

解压文件写入函数:

	bool ReleaseFiles(FILE* frp)
	{
        创建解压文件
		FILE* fwp;
        以二进制,写入,若没有该文件追加创建的形式打开
		fopen_s(&fwp, releaseFileName.c_str(), "wb");
		if (fwp == nullptr)return 0;

        用于储存读取压缩文件得到的二进制数据
		unsigned long long s;
		TreeNode* it = huaffTree->root;
		while (!feof(frp))
		{	
			int k=fread(&s, 1, sizeof(s), frp);
			long eight = 64;
			while (eight > 0&&((64-eight)/8)<= k)
			{
				int x = (s >> (64-eight)) & 1;
				if (x == 0)it = it->left;
				if (x == 1)it = it->right;
				if (!it->left && !it->right)
				{
					fwrite(&it->val, sizeof(it->val), 1, fwp);
					it = huaffTree->root;
				}
				eight--;
			}
		}
        完成解压缩操作
   
        关闭解压文件
		if (fwp)fclose(fwp);
		return 1;
	}

还原哈夫曼树函数:


	HuaffmanTree* ReCreateTree(FILE* fp)
	{
		读一个int类型,表示字符频率键值对数据的数目
		unordered_map<unsigned char, unsigned long long> weight;
		int mapSize = 0;
		if (fp == nullptr) return nullptr;
		fread(&mapSize, sizeof(mapSize), 1, fp);

        创建临时储存空间,读取到的键值对都会赋值给data
		pair<unsigned char, unsigned long long> data;

        循环读入键值对并将其放入哈希表中,直到读取全部键值对完毕
		while (mapSize>0)
		{
			fread(&data, sizeof(data), 1, fp);
			weight.insert(data);
			mapSize--;
		}
       
        根据创建的哈希表,调用哈夫曼树类的构造函数,还原哈夫曼树
		HuaffmanTree* huaffTree = new HuaffmanTree(weight);
		return huaffTree;
	}

 

获得解压文件名称后缀函数:

	void GetSuffixName(FILE* fp)
	{
        读取后缀字符个数
		int sufSize=0;
		fread(&sufSize, sizeof(sufSize), 1, fp);

        字符储存临时空间
		char s;
     
        循环读取后缀字符
		while (sufSize>0)
		{
			fread(&s, 1, 1, fp);
            将读到的字符加入releaseFileName中用于创建解压文件
			releaseFileName += s;
			sufSize--;
		}
	}

第四大类:Press定义在Press.h头文件中,用于和用户交互,同时处理文件名称,文件保存路径等问题的预处理类。

#pragma once
#include<iostream>
#include"PackPress.h"
#include"PackRelease.h"
using namespace::std;

class Press
{
public:
	Press() {};
	~Press() {};
	void Start(){}
private:
    处理需要压缩文件的名称,分离文件名,与文件类型名(既文件后缀)
	string GetSuffix(string &FileName){}
};

用户接口函数:

void Start()
	{
        文件保存路径名
		string path = "C:\\Users\\hwx\\Desktop\\TheInvisibleGuardian\\";
        
        压缩文件后缀
		string tempSuffixName = ".hwx";
       
        需要压缩文件完全名称包括文件类型
		string FileName;
        
        需要压缩文件类型名称既文件后缀
		string suffixName;

         需要压缩文件名称不包括文件类型名
		string PressName;
      
        压缩文件名称
		string tempName;
      
        解压文件名称
		string ReleaseName;
		int p = 0;
		cout << "文件默认路径为:" << path << endl;
		cout << "压缩文件请按:1   解压文件请按:2 修改文件默认路径请按:3" << endl;
		cout << "请选择需要进行的操作:";
		cin >> p;
		if (p == 1)
		{
			cout << "请输入需要压缩的文件名,包含文件后缀:";
			cin >> FileName;
			suffixName = GetSuffix(FileName);
			PressName = path + FileName + suffixName;
			tempName = path + FileName + tempSuffixName;
			PackPress H(PressName, tempName, suffixName);
		}
		else if (p == 2)
		{
			cout << "请输入需要解压的文件名";
			cin >> FileName;
			tempName = path + FileName + tempSuffixName;
			ReleaseName = path + FileName + "(解压版)";
			PackRelease D(tempName, ReleaseName);
		}
		else if (p == 3)
		{
			cout << "请输入新的文件默认路径";
			cin >> path;
			cout << "文件默认路径为:" << path << endl;
		}
		else
		{
			cout << "无效输入";
		}
	}
string GetSuffix(string &FileName)
	{
		int i = FileName.size() - 1;
		for (; i >= 0; i--)
		{
			if (FileName[i] == '.')
			{
				break;
			}
		}
		string s;
		int j = i;
		for (; i < FileName.size(); i++)
		{
			s.push_back(FileName[i]);
		}
		for (int f=FileName.size()-1; f>=j; f--)
		{
			FileName.pop_back();
		}
		return s;
	}

 

 

三、文件压缩和解压过程函数流程及函数调用顺序

 

压缩过程:

——Press构造函数

——Start函数

——GetSuffix(FileName)函数

——PackPress构造函数

——StartPress()函数

——(ReadFiles(weight)函数-CreateTree(weight)函数)

——(WriteFiles(weight)函数-TransLookTree()函数)

——完成压缩

 

解压缩过程:

——Press构造函数

——Start函数

——PackRelease 构造函数

——StartRelease()函数

——GetSuffixName(fp)函数

——ReCreateTree(fp)函数

——ReleaseFiles(fp)函数

——完成解压过程

 

程序测试

压缩文件格式mp3txtjpgmp4使用360压缩(mp4)
压缩前文件大小57mb12.1mb129kb1.35gb1.35gb
压缩后文件大小50.9mb9.3mb129kb1.35gb1.34gb
压缩时间1分10秒16秒28分钟1分钟
解压时间9秒1.989秒4分钟5秒

 

该程序压缩和解压缩时间与现有压缩软件相比较慢,最主要占用时间部分是读文件然后一个字符一个字符的进行操作,该过程压缩时重复两次,解压时重复一次,占用压缩和解压缩文件90%以上的时间.

 


总结

本文章仅记录或者分享该程序制作过程,由于个人能力有限,其中若有误,敬请指正,还望谅解

未经允许请勿转载。

 

综合实验: 1. 问题描述 利用哈夫曼编码进行通信可以大大提高信道利用率,缩短信息传输时间,降低传输成本。这要求在发送端通过一个编码系统对待传输数据预先编码,在接收端将传来的数据进行译码(复原)。对于双工信道(即可以双向传输信息的信道),每端都需要一个完整的编/译码系统。试为这样的信息收发站编写一个哈夫曼码的编/译码系统。 2. 基本要求 一个完整的系统应具有以下功能: (1) I:初始化(Initialization)。从终端读入字符集大小n,以及n个字符和n个权值,建立哈夫曼树,并将它存于文件hfmTree中。 (2) E:编码(Encoding)。利用已建好的哈夫曼树(如不在内存,则从文件hfmTree中读入),对文件ToBeTran中的正文进行编码,然后将结果存入文件CodeFile中。 (3) D:译码(Decoding)。利用已建好的哈夫曼树将文件CodeFile中的代码进行译码,结果存入文件Textfile中。 (4) P:印代码文件(Print)。将文件CodeFile以紧凑格式显示在终端上,每行50个代码。同时将此字符形式的编码文件写入文件CodePrin中。 (5) T:印哈夫曼树(Tree printing)。将已在内存中的哈夫曼树以直观的方式(比如树)显示在终端上,同时将此字符形式的哈夫曼树写入文件TreePrint 中。 3. 测试数据 用下表给出的字符集和频度的实际统计数据建立哈夫曼树,并实现以下报文的编码和译码:“THIS PROGRAME IS MY FAVORITE”。 字符 A B C D E F G H I J K L M 频度 186 64 13 22 32 103 21 15 47 57 1 5 32 20 字符 N O P Q R S T U V W X Y Z 频度 57 63 15 1 48 51 80 23 8 18 1 16 1
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值