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

  时间:2021-06-10 05:13:46  阅读量:397  评论数:0  作者:

本文介绍基于哈夫曼编码的C 压缩程序相关知识,这篇文章的作者不仅说明详细,还时不时举例代码分析,碰到坑的朋友可以阅读理解一下。

 

 

 


前言

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


 

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

压缩过程

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>
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;

用于记录字符对应哈夫曼编码路径,如a的哈夫曼编码为0101
struct path{};

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, path>> &map,TreeNode* root,int n,path x){}

哈夫曼路径储存结构:

struct path
{
	unsigned long long x = 0;
	unsigned long long y = 0;
};

注意:这里选择,自定义一个结构体,结构体内部使用两个8字节大小的数据类型来储存哈夫曼编码。

原因一:一个字符的哈夫曼编码最长为二进制255位,既32个字节大小,但是这种可能性较小,小于1G的文件基本上不会到这样长的编码,而大于1G的文件使用该程序压缩将会非常慢。考虑到写入压缩文件算法不能太复杂(太复杂写不出来),所以最多记录16个字节大小的哈夫曼编码。需要使用两个8字节大小数据储存哈夫曼编码

原因二:若动态数组储存,在写入压缩文件时,我们只能对哈夫曼编码一位一位的读取和操作,这样会大大增加时间复杂度,所以我们使用两个8字节大小数据类型,使用位运算操作,批量处理二进制位,既批量处理哈夫曼编码。加快写入速度

 

压缩构造函数:

	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, path>> &map,TreeNode* root,int n,path x)
	{
        递归遍历哈夫曼树,到叶子结点后,将字符对应哈夫曼编码放到哈希表中
		if (!root->right&&!root->left)
		{
			map.insert(make_pair(root->val,make_pair(n,x)));
			return;
		}
		TransLookTree(map, root->left,n+1,x);
		if (n < 64)  x.x |= ((unsigned long long)1 << (63 - n));
		else  x.y |= ((unsigned long long)1 << (127 - n));
		TransLookTree(map, root->right,n+1,x);
		return;
	}

 

注意:

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

这里不选择数组记录哈夫曼编码的原因,在哈夫曼编码路径储存自定义结构体struct path,那里有说明,此处不再赘述。

二、关键语句

if (n < 64)  x.x |= ((unsigned long long)1 << (63 - n));
        else  x.y |= ((unsigned long long)1 << (127 - n));

用于结构体中x,y用于实际记录哈夫曼编码,当哈夫曼编码大于64位,时使用y记录,所以一段较长哈夫曼编码可能储存在结构体x和y中。所以在写入哈夫曼编码时要考虑到这一点。

 

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

bool WriteFiles(unordered_map<unsigned char, unsigned long long>& weight)
	{
		创建哈夫曼编码的哈希表,哈夫曼编码表(空)
		unordered_map<unsigned char,pair<int,path>> map;  
       
        哈夫曼路径储存处
		path 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);
		}
		

        转换所有原文件字节信息为哈夫曼编码,并储存入压缩文件中
		以64二进制位位单位写入
        以下为合并每个字节的哈夫曼编码到64位二进制数据类型n中,并写入。
		int eight = 64;
		unsigned long long n=0;
		unsigned char s[10240] = { 0 };
		while (!feof(ifs))
		{
			int k=fread(s, 1,sizeof(s),ifs);
			int i = 0;
			while (i < k)
			{
				path x = map[s[i]].second;
				int k = map[s[i]].first;
				if (k <= 64)
				{
					if (eight > k) {
						n |= (x.x >> (64 - eight)); eight -= k;
					}
					else if (eight == k) {
						n |= (x.x >> (64 - eight));
						fwrite((char*)&n, sizeof(n), 1, ofs);
						n = 0;
						eight = 64;
					}
					else
					{
						n |= (x.x >> (64 - eight));
						fwrite((char*)&n, sizeof(n), 1, ofs);
						n = 0;
						n |= (x.x << eight);
						eight = 64 - (k - eight);
					}
				}
				else
				{
					int t = eight;
					n |= (x.x >> (64 - eight));
					fwrite((char*)&n, sizeof(n), 1, ofs);
					n = 0;
					eight = 64;
					n |= (x.x << t);
					eight -= t;
					if (eight > k - 64)
					{
						n |= (x.y >> (64 - eight));
						eight -= k;
					}
					else if (eight == k - 64) { n |= (x.y >> (64 - eight)); 
                    fwrite((char*)&n, sizeof(n), 1, ofs); n = 0; eight = 64; }
					else
					{
						n |= (x.y >> (64 - eight));
						fwrite((char*)&n, sizeof(n), 1, ofs);
						n = 0;
						n |= (x.y << eight);
						eight = 64 - ((k - 64) - eight);
					}
				}
				i++;
			}	
		}
        该句用来处理到,最后哈夫曼编码长度不够64位时,直接写入,可能会导致一些多余信息
        不过不影响原文件
		if(eight!=0) fwrite((char*)&n, sizeof(n), 1, ofs);

        哈夫曼编码写入完毕

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

 

注意:此处合并,并且写入哈夫曼编码算法比较复杂,篇幅有限不方便叙述。

 

 

第三大类: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))
		{	
            对压缩文件按每8个字节既64个二进制位进行读取,存入s中
			fread((char*)&s, sizeof(s),1, frp);
            
            用于判断s中储存的信息是否使用完毕
			int eight = 64;

            循环走哈夫曼树到达叶子结点时,将得到的字符写入解压文件中
			while (eight > 0)
			{
				int x = (s >> (eight - 1)) & 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%以上的时间.

 


总结

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

未经允许请勿转载。

 

关键词:数据结构 算法,基于,哈夫曼编码,编码,压缩,压缩程序,程序