项目记录--文件压缩1

1.文件压缩的概念

文件压缩是指在不丢失有用信息的前提下,所见数据量以减少存储空间,提高其传输,存储和处理效率,或者按照一定的算法对文件中数据进行重新组织,减少数据冗余和存储的空间的一种技术方法。

2.文件压缩的优点

a.紧缩数据存储容量,减少存储空间
b.可以提高数据传输的速度,减少带宽占用量,提高通讯效率
c.对数据的一种加密保护,增强数据在传输过程中的安全性

3.文件压缩的分类

我们根据解压缩的结构是否产生损害,将其分成无损压缩与有损压缩。
无损压缩是指解压缩的结构与被解压缩的内容是完全相同的,常见的有对文本文件的压缩。
有损压缩是指解压缩的结构不能完全被还原成与文件相同的结构,常见的有图片或者视频进行压缩。
一般情况下,有损压缩算法比无损压缩算法效率高。压缩率=压缩之后结构的大小/源文件大小 。

4.压缩的原理

压缩文件的本质:让文件占用的空间更小。
压缩文件的三种方式:
a.对于公众熟知的词汇利用更短的短语代替
优点:这种压缩较为简单。
缺点:提前需要准备好所有公众熟知的语句以及对应的短语。
b.LZ77,通用的压缩算法
有一些短语重复出现,对重复出现的短语利用更短的短语来进行压缩<距离,长度>,对于没有重复出现的内容,原封不动的王压缩文件中写入。
举个栗子:
给出源文件:accdesdertswccdew
压缩结果:accdesdertsw(11,3)ew
通过无损的压缩算法:accdesdertswccdew
c.对原文件中的字节找一个更短的比特位编码来进行替换
文件中的数据最终是在磁盘上都是以字节方式来进行存储的,一个字节对应8个比特位,如果将源文件中的数据往磁盘中写的时候,每个字节如果都能够找到更短的比特位来进行替换,可以达到压缩文件的目的。它的核心也就是如何给文件中的字节找对应其的更短的编码。
举个栗子:
源文件:ABCDBCDBCDCDCDDD
通过观察源文件发现,源文件总共占了16个字节,但是字符种类一共有4种,因此只需要两个比特位来进行表示。以下是源字符与其对应的二进制编码。
A–00 B–01 C–10 D–11
接下来对每个文件进行压缩:只需要用每个字节对应的二进制编码改写源文件就可以了,因此最后的结果是:00011011011011011011101110111111,最终解压缩文件时将要文件还原成与源文件完全相同的内容,是无损的。
根据上面的例子,我们进一步改进,一般情况下使用的都是不同长的编码进行提花的,不等长编码的方式可以达到更好的压缩率。不等长编码是指编码中比特位个数不同,在这里我们要保证让出现多的次数的字节对应的编码更短一些。
现在我们来看原文件中字节出现的频次:A–1;B–3;C–5;D–7;此时我们可以一字节和其出现 的次数作为叶子节点求构造一棵二叉树。
在这里插入图片描述
创建好树之后,令左分支为0,右分支为1,获取编码是从根遍历到也直接点所经路径,得到编码如下表1:

字符编码
A100
B101
C11
D04

基于Huffamn编码的文件压缩的压缩方式:

  1. 获取源文件中每个字节出现的次数
  2. 跟进字节频次信息,构建二叉树(构建Huffman树)
  3. 获取编码
  4. 使用获取到的字节的编码对源文件进行改写

5.Huffman的理解与构建

5.1Huffman树定义

从二叉树根节点到二叉树所有叶节点的路径长度与所有权值的乘积之和为该二叉树的带权路径长度WPL。具有最小带权路径长度的二叉树称之为Huffman树,也称之为最优树。
如下所示:
在这里插入图片描述

WPL1 = (7+5)3+32+11 =43;WPL2 = 71+5*2+(1+3)*3=29;从这个结构中我们可以知道如何将做到带权路径最短,就是让权值大的节点离得近一点,让权值晓得节点离根节点远一些。

5.2构建Huffman树

  1. 由给定的n个权值{W1,W2,…,Wn }构造n棵只有根节点的二叉树, 从而得到一个二叉树的集合F={T1,T2,…,Tn }。这就相当于给的权值来构造节点,然后将节点保存起来。
  2. 在F中选取根节点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根节点的权值为其左、右子树根节点。
  3. 权值之和。 在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉 树加入到集合F中。
  4. 重复2,3两步,当F中只剩下一棵二叉树时,这棵二叉树 便是所要建立的哈夫曼树。
#include<queue>
#include<vector>
using namespace std;


//保存二叉树中的节点
template<class W>//W代表的是权值
struct HuffmanNode {
	HuffmanNode<W>* left;
	HuffmanNode<W>* right;

	W weight;
	HuffmanNode(const W& w = W()):left(nullptr)
						   ,right(nullptr)
						   ,weight(w){}
};

//我们自己写一个比较函数
template<class W>
struct compare {
	typedef huffmanNode<W> Node;
	bool operator()(const Node* left, const Node* right) {
		return left->weight > right->weight;
	}
};

template<class W>
class huffmanTree{
	typedef HuffmanNode<W> Node;
public:
	huffmanTree() :root(nullptr) {}
	void CreateHuffmanTree(const W array[], size_t size) {
		//优先级队列默认情况下树大堆,我们要修改其比较规则,将其改成小堆
		std::priority_queue<Node*> , vector<Node*> , compare<W>> q;
        //我们先使用所给的权创建只有根节点的二叉树森林,我们采用优先级队列进行保存
		for (size_t i = 0; i < size; i++) {
			q.push(new Node(array[i]));
		}
		//循环进行以下步骤,直到我们所给的二叉树森林中只剩下一棵二叉树为止
		while (q.size() > 1) {//从二叉树森林中先去权值最小的两棵二叉树
			Node* left = q.top();
			q.pop();

			Node* right = q.top();
			q.pop();
			//将left和right作为某个新的节点的左右孩子构造一个新的二叉树,新二叉树根节点的权值就是七左右孩子权值之和
			Node* parent = new Node(left->weight, right->weight);
			parent->left = left;
			panret->right = right;
			//将新的二叉树插入到二叉树森林中
			q.push(parent);
		}
	//循环结束后,就是我们所需要的huffman树
		root = q.top();
	}
	void destory(Node* &p){//销毁树,不销毁会产生内存泄漏
		if (p) {
			destory(p->left);
			destory(p->right);
			delete p;
			p = nullptr;
		}
	}
private:
	Node* root;
};

在这里我们遇到了一个问题,如下:
在这里插入图片描述
在进行调节大堆成为小堆的时候并没有成功,原因是在将大堆变成小堆的时候,我们二叉树比较的并非权值的大小,而是比较的是地址的大小,此时地址是一个小堆,但是权值并非是是小堆。

六.文件压缩

6.1压缩流程

  1. 统计源文件中每个字节出现的次数
    因为fileByteInfo中每个元素的ch的ASCII值刚好与数组的下标时一一对应的,因此在统计时就可以直接以字节的ASCII码作为数组的下标来进行统计。
bool FileCompress::FileCompressFile(const string& filePath){
	//1.统计源文件中每个字节出现的次数并进行保存
	FILE* pf = fopen(filePath.c_str(), "r");
	if (nullptr == pf){//检测打开文件是否成功
		//当pf为空,则打开待压缩文件失败
		cout << "open default";
		return false;
	}
	//打开成功之后,就可以进行统计,将原文件中内容进行读取
	//由于不知道文件的大小,此时我们要才有循环的方式来获取源文件中的内容
	char readBuff[1024];
	while (true){
		//原本应该读取1024个字节,但是实际上我们读取到了rdsize个字节
		size_t rdsize = fread(readBuff, 1, 1024, pf);
		if (0 == rdsize){//当读取到的文件的末尾了
			break;
		}
		//进行统计
		for (size_t i = 0; i < rdsize; ++i){
			//利用到了哈希里面的直接定值法--以字符ASCII值作为数组的下标来进行快速统计
			//fileByteInfo中每个元素的ch的ASCII值刚好与数组的下标是一一对应的,因此在统计时就可以直接以字节的ASCII码作为数组的下标来进行统计
			fileByteInfo[readBuff[i]].appareCount++;
		}
	}
}
  1. 根据统计的结果来创建Huffman树
    根据所给的权值创建n个只有根结点的二叉树森林,使用priority_queue保存二叉树,并且此时必须是小堆;然后进行循环操作:从森林中获取根结点的权值最小的两棵二叉树,一这两棵二叉树作为某个节点的左右孩子创建一棵新的二叉树,新二叉树根的权值就是器左右孩子节点中权值之和;最后将新二叉树放回二叉树森林中。上面的操作,直到二叉树森林中只剩余一颗二叉树为止。
bool FileCompress::FileCompressFile(const string& filePath){
	//2.根据统计的结果创建Huffman树
	//注意,在创建Huffman树的过程中,必须将次数出现为0的删掉
	huffmanTree<ByteInfo> ht;
	ByteInfo invalid;
	ht.CreateHuffmanTree(fileByteInfo, 256, invalid);
}
  1. 通过Huffman树来湖区每个字节的编码
    获取编码实际上就是遍历二叉树,因此我们在这里选择一种方法,在走到叶子节点的位置之后在进行获取Huffman编码。
bool FileCompress::FileCompressFile(const string& filePath){
	//3.借助Huffman树获取每个字节的编码
	GenerateHuffmanCode(ht.GetRoot());
}
void FileCompress::GenerateHuffmanCode(HuffmanNode<ByteInfo>* root){
	if (nullptr == root) return;//如果树是空的,那么直接返回
	//如果树是非空的话,那么就进行遍历
	//因为再Huffman树当中,所有有效的权值在叶子节点的位置
	//当遍历到叶子节点的位置时,该权值对应的编码应该就拿到了
	if (nullptr == root->left && nullptr == root->right){
		//此时说明时叶子节点,左右节点都为空
		HuffmanNode<ByteInfo>* cur = root;
		HuffmanNode<ByteInfo>* parent = cur->parent;
		//因为最终获取到的 编码需要保存到fileByteInfo中
		string& strCode = fileByteInfo[cur->weight.ch].strCode;//定义字符的编码
		while (parent){
			if (cur == parent->left){
				//如果此时的节点时其父节点的左孩子
				strCode += '0';
			}
			else{//如果是右孩子
				strCode += '1';
			}
			cur = parent;
			parent = cur->parent;
		}
		//当循环结束后,获取的编码时倒着的,因此我们要对他进行逆置
		reverse(strCode.begin(), strCode.end());
	}
	GenerateHuffmanCode(root->left);
	GenerateHuffmanCode(root->right);
}
  1. 使用获取的字节的编码对源文件进行改写
    使用湖区道德字节编码对源文件进行改写,即对源文件中的每个字节直接进行替换,替换成该字节对应的二进制格式的Huffman编码。
bool FileCompress::FileCompressFile(const string& filePath){
	//4.使用字节的编码队员文件重新进行改写
	//创建压缩结果文件
	//注意,在后续读取pf文件时,需要将pf文件指针挪动到文件起始位置
	//因为感慨是在统计文件中字节出现次数的时已经读取过一遍文件了,pf已经在文件的末尾了
	rewind(pf);//将pf指针移动到起始位置
	FILE* fOut = fopen("2.hzp","w");
	char ch = 0;
	char bitCount = 0;//用来记录现在拥有的比特位有效
	while (true){
		size_t rdsize = fread(readBuff, 1, 1024, pf);
		if (0 == rdsize) break;
		//用编码改写字节,改写的结果需要放置到压缩结果的文件
		for (size_t i = 0; i < rdsize; ++i){
			string& strCode = fileByteInfo[readBuff[i]].strCode;
			//只需要将字符串格式的二进制编码往字节中存放
			for (size_t j = 0; j < strCode.size(); ++j){
				ch <<= 1;//高位丢弃
				if ('1' == strCode[j]){
					ch |= 1;
				}
				//当ch中的8个比特位填充满之后,需要将该字节写入到压缩文件当中
				bitCount++;
				if (8 == bitCount){
					fputc(ch, fOut);
					bitCount = 0;
				}
			}
		}
	}
	//检测:当ch不够8个比特位,实际上是没有写进去的
	if (bitCount > 0 && bitCount < 8){ 
		ch <<= (8 - bitCount);//因为有效的解压缩是要从高位向低位来进行解压缩
		fputc(ch , fOut);
	}
	fclose(pf);
	fclose(fOut);
	return true;
}
  1. 压缩文件的格式
    压缩文件的格式需要压缩数据,即最终源文件转换成的Huffman编码文件,还需要解压缩文件的信息。根据保存的Huffman编码我已拥有压缩的数据,现在可以使用表一中的内容用做压缩信息,此时通过逐个比特位获取,然后在表一中查找,如果没有找到,就继续获取下一个比特位,如果找到了就解压缩成功。但是使用这种方法他的效率极其低。在这里hi有另一种方法来进行解压缩。根据压缩数据对应的二进制比特流来遍历Huffman树。我们在进行解压缩的过程中使用Huffman树来进行解压缩。开始的时候给出一个指针cur,将指针放在根节点的位置,然后依次获取每一个比特位如果该比特位为0,则让cur往器左子树移动,如果该比特位为1,则让cur往其右子树移动,只要cur走到叶子节点的位置,就解压缩成功了一个字节。在这里我们知道要进行解压缩要得到他的Huffman树,因此我们只需要将Huffman树进行还原就行,因此解压缩的信息中只需要保存字节出现的频次信息。
    在这里插入图片描述
bool FileCompress::FileCompressFile(const string& filePath){
	//5.写解压缩文件的时候需要用大的信息
	WriteHead(fOut, filePath);
}
void FileCompress::WriteHead(FILE* fOut, const string& filePath){
	//1.先获取源文件中的后缀
	string posFix = filePath.substr(filePath.rfind('.'));
	posFix += "\n";
	fwrite(posFix.c_str(), 1, posFix.size(), fOut);//将后缀写入

	//2.构造字节的频次信息以及有效的字节行数
	string chAppearCount;
	size_t lineCount = 0;
	for (size_t i = 0; i < 256; ++i){
		if (fileByteInfo[i].appareCount > 0){
			chAppearCount += fileByteInfo[i].ch;
			chAppearCount += ":";
			chAppearCount += to_string(fileByteInfo[i].appareCount);
			chAppearCount += "\n";
			lineCount++;
		}
	}
	//3.写总行数以及频次信息
	string totalLine = to_string(lineCount);
	totalLine += "\n";
	fwrite(totalLine.c_str(), 1, totalLine.size(), fOut);//将后缀写入
}

最后的文件如下图:在这里插入图片描述

6.2在压缩文件中遇到的问题

question1:当我在直接给Huffman树进行实例化的时候,编译器报错了。
在这里插入图片描述
在进行文件压缩的时候,创建Huffman树失败,通过查看错误信息,我发现是在实例化当中权值相加会进行报错,原因其实很简单,因为在写的Huffman树的代码中进行检测的时候,权值相加都是int型,返回也是整形,因此编译器不会报错,而在进行实例化当中我们给的是结构体类型(ByteInfo),此时相当于两个对象进行相加,在相加的过程中编译器无法选择相加的规则,因此会报错。解决的方式十分简单,只需要在ByteInfo结构体当中对+进行重载就可以了。而在进行比较的时候也会遇到问题,因此也需要进行对比较的字符进行重载。
question2:创建Huffman树完成后,发现有的节点是错误的。
在这里插入图片描述
我们从上面看到程序创建好的Huffman树中4的左侧实际不是叶子节点,这是我们知道出现了错误。因为在程序中创建Huffman树的时候,我们将出现次数为0的字节放到Huffman树当中了,而此时我们并不需要这些字节,因此我们要出去出现次数为0的字节。
question3:在我进行到将字符转化成他的Huffman编码时,出现了错误。
在这里插入图片描述
在得到的文件当中他的内容是空的,这是错误的。原因是在刚刚开始统计文件中字节出现的次数的时候已经读取去过一次了,此时文件指针应该是指向文件末尾的。因此在后续读取时,因该将文件指针挪动到文件起始的位置。我们可以使用rewind()函数。
question4:源文件在转化成Huffman编码时没有完全转化,出现错误。
在这里插入图片描述

在我进行完上面的步骤之后,将源文件转化成Huffman编码后我查看了转成Huffman编码的文件,发现里面的内容并没有完全转化,在我所给的示例中只有前三个个字节,原本应该是0x96,0xDF,0xFC,0x00,现在少了最后一个字节。因为在最后一次在修改完之后,ch的有效位不一定都是8个比特位,因此有可能会漏掉一些,导致没有写进去。因此我们可以在最后进行检测,将最后剩下的补上。
检测的代码:

//检测:当ch不够8个比特位,实际上是没有写进去的
	if (bitCount > 0 && bitCount < 8){ 
		ch <<= (8 - bitCount);//因为有效的解压缩是要从高位向低位来进行解压缩
		fputc(ch , fOut);
	}

6.3解压缩文件

  1. 从压缩文件中读取解压缩时需要的信息,将信息还原
bool FileCompress::UnFileCompressFile(const string& filePath){
	//1.从压缩文件中读取解压缩时需要的信息,就是将信息还原
	FILE* fIn = fopen(filePath.c_str(),"r");
	if (nullptr == fIn){//判断文件有没有正常打开
		cout << "open default" << endl;
		return false;
	}
	//读取源文件后缀
	string postFix;
	Getline(fIn, postFix);
	//读取频次信息的总行数
	string strContent;
	Getline(fIn, strContent);
	//此时他还是一个字符串,则将它转换成行数
	size_t linecount = atoi(strContent.c_str());
	//循环获取lineCount行的字节频次信息
	for (size_t i = 0; i < linecount; ++i){
		strContent = "";//先将他进行清空
		Getline(fIn, strContent);
		fileByteInfo[strContent[0]].ch = strContent[0];
		fileByteInfo[strContent[0]].appareCount = atoi(strContent.c_str() + 2);//得到次数,因为真正的数字实在字节后面
	}
}
void FileCompress::Getline(FILE* fIn, string& strCount){

	char ch;
	while (!feof(fIn)){//文件指针不在文件的末尾,则循环继续
		ch = fgetc(fIn);
		if (ch == '\n'){
			break;
		}
		strCount += ch;
	}
}
  1. 恢复Huffman树
//2.恢复Huffman树
	huffmanTree<ByteInfo> ht;
	ByteInfo invalid;
	ht.CreateHuffmanTree(fileByteInfo, 256, invalid);
bool FileCompress::UnFileCompressFile(const string& filePath){
	//3.读取压缩数据,对Huffman树进行解压缩
	string filename("3");//创建解压缩文件
	filename += postFix;
	FILE* fOut = fopen(filename.c_str(),"w");
	char readBuff[1024];
	char bitCount = 0;
	HuffmanNode<ByteInfo>* cur = ht.GetRoot();
	int fileSize = cur->weight.appareCount;//此时我们还可以从Huffman树中知道根结点的权值就是文件的总大小
	int compressSize = 0;//待解压缩的字节数
	while (true){
		size_t rdsize = fread(readBuff, 1, 1024, fIn);
		if (0 == rdsize)//当读到文件末尾
			break;
		//对读到的比特位一个一个来进行处理
		for (size_t  i = 0; i < rdsize; ++i){
			//逐个字节比特位来进行解压缩
			char ch = readBuff[i];
			bitCount = 0;//让他重新恢复到0
			while (bitCount < 8){
				if (ch & 0x80){//让他与1000 0000进行与操作,这样是用来检测ch的高位,他只保留高位的数字,其他位都为0,当高位为1,结果就是1
					cur = cur->right;//如果结果为1,则向根结点的右子树走
				}else{
					cur = cur->left;//结果位0,则向根结点的左子树走
				}
				
				//在结束玩上面的判断后检测此时是否到了叶子节点
				if (nullptr == cur->left && nullptr == cur->right){
					//如果此时在叶子节点的位置,那么我们就解压缩出来了一个字符
					fputc(cur->weight.ch,fOut);
					cur = ht.GetRoot();
					compressSize++;
					//当待解压字节的个数与文件的大小相等的时候,停止解压缩,解压缩成功
					if (compressSize == fileSize){
						break;
					}
				}
				bitCount++;
				ch <<= 1;//当将前一个比特位检测完成之后,要将高位移走
			}
		}
	}
	fclose(fIn);
	fclose(fOut);
	return true;
}

6.4解压缩文件中遇到的问题

question1:在解压缩完成后得到的文件最后结果与源文件的内容不相同,出现了错误。
在这里插入图片描述
从解压缩后文件中我们发现,最后的内容中的字符D要比源文件中多,这是不对的。这是因为在进行压缩的时候最后一个字节不够8个比特位时我们进行了特殊的处理,而真正的时只有5个比特位时有效的,但是在解压缩期间我忘记处理这些无效的比特位,有些时候在解压缩的过程中并不需要解压缩所有的比特位。导致了多解压缩了3个比特位。解决这个问题很简单,我们只需要在当待解压字节的个数与文件的大小相等的时候,停止解压缩,此时解压缩就会成功。
在这里插入图片描述
6.5测试时遇到的一些问题
在将大概的流程处理结束后,我们还有一些小的问题需要处理。
question1:我在源文件中加入了有汉字的时候,代码运行是会崩溃的。
在这里插入图片描述

在这里strCode什么都没有拿到,那就说明strCode时无效的。错误的原因时因为如果给的字符都是原ASCII码中的字符,那么他的数组下标都是正数,但是如果是汉字不存在ASCII码中,因此会出错。汉字一般情况下是由多个字节来进行表示的,而且每个字节都排在ASCII表中的字符之后,那么他拿到的数字就有可能会超过128,若果超过128,那么我们拿到的结果就会为负数。这个问题的解决方式就是将程序中所有的void类型全部换成unsigned char。
question2:
在我进行测试的时候,发现多行文本运行时会出现乱码。
在这里插入图片描述
此时我们发现多行文本在解压缩时出现了乱码。在这里我们知道了解压缩文本时错误的,这里我用最直接的排错方法,直接检查在压缩与解压缩时用到的Huffman树是否一样,实际上我们只需要比较压缩与解压锁时所要用到的频次信息是否一样。在这里我发现他们使用的并不相同。在后面的查找中我发现字符"\n"除了问题。其实最终也是我写的Getline()方法出了问题。因此我们必须对这个方法进行特殊处理。在这里我们要住以换行字符处理的时候容易出现问题,最后一次循环会少一次,结果导致压缩和解压缩时用到的字节频次信息不同而导致解压缩出错,应该特别注意。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值