文件压缩-HuffMan压缩

什么是HuffMan压缩:
简单来说就是利用Huffman树生成Huffman编码,对文件重复出现的字符进行记录,以减少出现次数。从而达到压缩文件的目的。
为什么HuffMan就能实现文件压缩呢?
数据在硬盘中的存储是有格式的,比如说字符就是char类型的,占了8个比特位,但是实际上有些字符可能根本就用不了8个比特位,因此就造成了空间的浪费。而huffman就是根据字符出现的次数重新建立存储规则,减少这些空间浪费。
什么是HuffMan树?

定义: Huffman树,又称最优二叉树,是加权路径长度最短的二叉树。
带权路径长度 = 节点的权值 * 当前节点距离根节点的路径长度
在这里插入图片描述

如何构建HuffMan树?

在这里插入图片描述

生成HuffMan编码

HuffMan树中左子树路径标记为0,右子树路径标记为1.从根节点到叶子结点的编码就是该字符的Huffman编码。
在这里插入图片描述

HuffMan文件压缩原理:

1.统计待压缩文件中每个字符出现的次数

  • 构建结构体,存放信息:字符,次数,编码
  • 遍历源文件,将每个字符的次数写入对应的结构体信息中

2.将字符出现的次数作为权值构建Huffman树

  • 普通的二叉树只能通过双亲节点找到子节点,但是在获取编码的时候需要通过叶子结点往根节点走,所有构建二叉树时,要有子节点指向双亲节点

3.通过Huffman树获取每个字符所对应的编码

  • 通过叶子结点找根节点的编码是逆序的,所以要使用reverse

4.向压缩文件中写入信息

  • 压缩文件中只保存压缩的文件内容是不够的,还要有一些记录源文件内容的信息
  • 第一行:存放源文件的后缀,因为在解压缩文件的时候需要后缀
  • 第二行:存放源文件字符,次数 的总行数
  • 第三行:存放字符和字符出现的次数–为了解压缩重建Huffman树
  • 剩余内容是压缩编码
    例如:源文件名为“1.txt”,存放“ABBBCCCCCDDDDDDD”
    在压缩文件中的模型为:
    在这里插入图片描述
解压缩:

1.获取后缀
2.获取字符以及对应的次数
3.重建Huffman树
4.解压压缩数据

  • 从压缩文件中读取一个字节ch
  • 从根节点开始,按照ch的8个比特位信息(代表字符的编码)从高到低遍历Huffman树:
    a.该比特位是0,取当前节点的左孩子,否则取右孩子
    b.一直遍历,直到遍历到叶子节点位置,该字符就被解析成功,讲解压出来的字符写入文件
    c.如果在遍历Huffman过程中,8个比特位已经比较完毕还没有到达叶子节点,就从第四步开始执行
    d.重复以上过程,直到所有的数据解析完毕
问题:

1.压缩汉字时候程序会崩溃,但是压缩字母程序就能正常运行?

  • 原因:创建存放信息的数据是char 类型的,类型大小是1字节(0~255),但是汉字都是占两个字节的,对应的char类型就会因为超过255而导致数组的下边变成负数,数组下边越界访问导致程序崩溃。
  • 解决方法:将存放信息的数据类型改成unsigned char 类型

2.解压大文件,只能解压缩一部分内容?

  • 原因:一般情况下,文件指针碰到EOF就表示到文件结尾了,因为EOF是 -1,也就是FF,所以只解压了一部分(解压到第一个FF就停止了)。
  • 解决方法:采用 feof()函数,多加一个判断即可,feof()函数就是判断文件末尾,而不仅仅是碰见EOF停止。

3.为什么压缩照片的时候会失败?

  • 原因:是因为 ‘\0’ 的问题,因为如果刚开始把(字节,次数)先写入 buf 中,再由 buf 通过 fwrite 函数写入文件中,一定会出现问题(可能出现 0 字节)。因为 fwrite 的第一个参数要求C格式的字符串,把 buf 转化为C格式的字符串,如果遇见 ‘\0’,就会停止,所以就会崩溃。
  • 解决方法:不要将字节写入 buf 中,而是通过 fputc 直接把字节写入文件中,然后再写入 buf 中,再将buf写入文件即可。

文件压缩的图解过程:

在这里插入图片描述


源码:
main.c

#include "HuffmanTree.hpp"
#include "FileCompressHuffman.h"

int main()
{
	//TestHuffman();

	FileCompressHuffmanM test;
	//test.CompressFile("文件压缩(原文件).png");

	test.UnCompressFile("文件压缩.hzp");
	return 0;
}



huffmanTree.hpp

#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<queue>


template<class W>
struct HuffmanTreeNode   //定义哈弗曼树节点的类型
{
	HuffmanTreeNode(const W& weight)
		:_pLeft(nullptr)
		, _pRight(nullptr)
		, _pParent(nullptr)
		, _weight(weight)
	{}

	HuffmanTreeNode<W>* _pLeft;
	HuffmanTreeNode<W>* _pRight;
	//用孩子双亲形式表示二叉树,方便从叶子结点找到根节点,从而对字符进行编码
	HuffmanTreeNode<W>* _pParent;

	W _weight;   //节点权值
};

template<class W>
struct Compare //用仿函数比较,变小堆
{
	typedef HuffmanTreeNode<W>* PNode;
	bool operator()(const PNode pLeft, const PNode pRight)
	{
		return pLeft->_weight > pRight->_weight;
	}
};


template<class W>
class HuffmanTree
{
	typedef HuffmanTreeNode<W> Node;
	typedef Node* PNode;
public:
	HuffmanTree()//构造函数,初始状态下为空树
		: _pRoot(nullptr)
	{}

	void CreatHuffmanTree(const std::vector<W>& v, const W& invalid)  //根据权值创建树,模板引用   vector在标准的命名空间中定义的
	{
 		if (v.empty())   //v是存放权值的数组
			return;
		//用所给的权值创建二叉树森林
		//std::priority_queue<PNode> q;    使用仿函数实现 小堆  比较器
		std::priority_queue<PNode,std::vector<PNode>,Compare<W>> q;//优先级队列 保存树,把地址放进去就好了,,但是默认是大堆,而我们需要的是小堆

		for (size_t i = 0; i < v.size(); ++i)
		{
			if (v[i] != invalid)  //过滤出现0次的字符,通过创建哈弗曼树多加上一个参数 来实现
			q.push(new Node(v[i]));  //用权值创建节点
		}
		while (q.size() > 1)  //当树不止一个时,把作为左右孩子
		{
			PNode pLeft = q.top();
			q.pop();

			PNode pRight = q.top();
			q.pop();

			PNode pParent = new Node(pLeft->_weight + pRight->_weight);
			
			pParent->_pLeft = pLeft;
			pLeft->_pParent = pParent;

			pParent->_pRight = pRight;
			pRight->_pParent = pParent;
			q.push(pParent);   //把两个子树生成的树放回队列中
		}
		_pRoot = q.top();   //哈夫曼树创建成功
	}

	PNode GetRoot()
	{
		return _pRoot;
	}

	~HuffmanTree()
	{
		_Destroy(_pRoot);   //销毁 哈夫曼树
	}

private:
	void _Destroy(PNode& pRoot)   //后序遍历进行销毁二叉树
	{
		if (pRoot)
		{
			_Destroy(pRoot->_pLeft);
			_Destroy(pRoot->_pRight);
			delete pRoot;
			pRoot = nullptr;
		}
	}

private:
	PNode _pRoot;
};


//void TestHuffman()
//{
//	std::vector<int> v{ 3,1,7,5 };
//	HuffmanTree<int> ht;
//	ht.CreatHuffmanTree(v);
//}

FileCompressHuffman.h

#pragma once
#include<string>
#include<iostream>
#include<vector>
#include "HuffmanTree.hpp"
using namespace std;

typedef unsigned char UCH;

//哈夫曼树中的字符信息,包括字符代表的字母,次数,编码
struct CharInfo
{
	//用构造函数初始化哈夫曼树中的字符
	CharInfo(size_t charCount = 0)
		: _charCount(charCount)
	{}

	//权值在结构体中,而结构体不能直接相加,所以要重载+
	CharInfo operator+(const CharInfo& info)  //字符出现的次数
	{
		return CharInfo(_charCount + info._charCount);
	}

	bool operator>(const CharInfo& info)
	{
		return _charCount > info._charCount;
	}

	bool operator!=(const CharInfo& info)const
	{
		return _charCount != info._charCount;
	}
	bool operator==(const CharInfo& info)const
	{
		return _charCount == info._charCount;
	}

	UCH _ch;
	size_t _charCount;
	std::string _strCode;
};

class FileCompressHuffmanM
{
public:
	FileCompressHuffmanM();
	void CompressFile(const std::string& strFilePath);  //const 保证原文件不会被修改
	void UnCompressFile(const std::string& strFilePath);//不加命名空间会如何?
	void WriteHead(FILE* fOut, const std::string& strFilePath);//头部信息:用来保存压缩文件的信息,包括后缀,字符出现的次数

private:
	void GetHuffmanCode(HuffmanTreeNode<CharInfo>* pRoot);
	void GetLine(FILE* fIn, std::string& strContent);
	std::vector<CharInfo> _charInfo;    //把编码保存在数组中,这个数组是个结构体类型的,字符信息

};

FileCompressHuffmanM.cpp

#pragma once
#include "FileCompressHuffman.h"
#include"HuffmanTree.hpp"
#include <iostream>
#include <assert.h>
using namespace std;

FileCompressHuffmanM::FileCompressHuffmanM() 
{
	_charInfo.resize(256);
	for (size_t i = 0; i < 256; ++i)
	{
		_charInfo[i]._ch = i;  //i是ch 的ASCII码
	}
}


void FileCompressHuffmanM::CompressFile(const std::string& strFilePath)
	{	 
        string MiddleName = strFilePath.substr(0, strFilePath.find('.'));// MiddleName是生成的压缩文件的名称
		//1.获取原文件中每个字符出现的次数
		FILE* fIn = fopen(strFilePath.c_str(), "rb");  //string 类 转换成char*类型
		if (fIn == nullptr)
		{
			cout << "文件打开失败" << endl;
			return;
		}

		UCH *pReadBuff = new UCH[1024];   //每次读取1024字节,即1M
		//long long CharCount[256] = { 0 };   //符号有可能出现很多次,所以出现次数用long long  类型表示
		
		//vector<CharInfo> charInfo(256);
		//初始化_ch

		while (1)
		{
			size_t rdSize = fread(pReadBuff, 1, 1024, fIn);//返回读到的元素个数
			if (0 == rdSize)
				break;
			for (size_t i = 0; i < rdSize; ++i)
			{
				//CharCount[pReadBuff[i]]++; //pReadBuff[i]表示第i个字节读到的内容,对应的ascii码作为数组CharCount的下标
				_charInfo[pReadBuff[i]]._charCount++;
			}
		}
		//2.以每个字符出现的次数作为权值构建哈夫曼树
		HuffmanTree<CharInfo> ht;   //类名 类型 实例化对象
		ht.CreatHuffmanTree(_charInfo,CharInfo(0));

		//3.根据哈弗曼树获取每个字符的编码
		GetHuffmanCode(ht.GetRoot());

		//4.根据每个字符的编码重新改写原文件
		//先打开一个文件,写入压缩信息
		MiddleName += ".hzp";
	
		FILE* fOut = fopen(MiddleName.c_str(), "wb");
		assert(fOut);

		WriteHead(fOut, strFilePath);
		UCH ch = 0;
		char bitCount = 0;
		fseek(fIn, 0, SEEK_SET);

		while (true)
		{
			size_t rdSize = fread(pReadBuff, 1, 1024, fIn);  //把buff中的编码写入压缩文件
			if (0 == rdSize)
				break;

			for (size_t i = 0; i < rdSize; ++i)
			{
				string& strCode = _charInfo[pReadBuff[i]]._strCode;
				// ch: 0000 0000
				// A:100
				// B:101
				for (size_t j = 0; j < strCode.size(); ++j)  //j小于  对应的字符的字符编码的长度
				{
					ch <<=1;
					if (strCode[j] == '1')   //ch初始值是0, ch代表一个字节,不是代表一个比特位,把这八个比特位填满就可以以往压缩文件中写入字节了
						ch |= 1;
					bitCount++;
					if (8 == bitCount)   //每满一个字节,就往压缩文件中写入一个字节
					{
						fputc(ch, fOut);
						bitCount = 0;
					}
				}
			}
		}
		//如果8个bit没有写满,还要把剩余的内容写入压缩文件
		if (bitCount >0 && bitCount < 8)   //大于0的原因是  ,如果8个比特位恰好写满,在上一步中会置0
		{
			ch <<= 8 - bitCount;
			fputc(ch , fOut);
		}

		delete[] pReadBuff;
		fclose(fIn);
		fclose(fOut);
 	}

void FileCompressHuffmanM::UnCompressFile(const std::string& strFilePath)
{
	//获得解压缩文件的名称
	string ResultName = strFilePath.substr(0,strFilePath.find('.'));

	//检测压缩文件的后缀格式
	string strPostFix = strFilePath.substr(strFilePath.rfind('.'));  //有两个参数,第二个默认结尾
	if (".hzp" != strPostFix )
	{
		cout << "压缩文件的格式有问题" << endl;
		return;
	}

	//获取解压缩的信息
	FILE* fIn = fopen(strFilePath.c_str(), "rb");     //fIn 是指向压缩信息 hzp文件   的指针
	if (fIn == nullptr)
	{
		cout << "压缩文件打开失败" << endl;
		return;
	}

	//获取原文件的后缀----读完压缩信息的第一行就行了,第一行保存的就是后缀
	strPostFix = "";
	GetLine(fIn, strPostFix);

	//获取总行数
	string strContent;
    GetLine(fIn, strContent);  //获取在遇到\n前的第一个内容,即行数,此时fIn 指向第一个\n
	size_t lineCount = atoi(strContent.c_str());

	//字符信息
	for (size_t i = 0; i < lineCount; ++i)
	{
		strContent = "";
		GetLine(fIn, strContent);  ??????     fIn此时不是指向的是\n么?
		if (strContent.empty())   //读取到换行符,如果不处理会直接退出,没有读取到后面的压缩内容
		{
			strContent += '\n';
			GetLine(fIn, strContent);
		}
		_charInfo[(UCH)strContent[0]]._charCount = atoi(strContent.c_str()+2); //获取到次数 获取的字符串是 A,1 字符A对应的是获取的字符串的下标为0 的元素 占据了两个字节,要偏移过去
	}              //strCont 中保存的是字符信息  如果是汉字  也有可能出现负数的情况,所以强转成无符号类型

	//还原哈夫曼树
	HuffmanTree<CharInfo> ht;
	ht.CreatHuffmanTree(_charInfo, CharInfo(0));

	//解压缩
	//string  strUNComFile = NameOfCompressFile;
	string strUNComFile = ResultName;

	strUNComFile += strPostFix;  //名字+后缀
	FILE* fOut = fopen(strUNComFile.c_str(), "wb");
	assert(fOut);

	char* pReadBuff = new char[1024];
	HuffmanTreeNode<CharInfo>* pCur = ht.GetRoot();
	char pos = 7;
	size_t SizeOfFile = pCur->_weight._charCount;


	while (true)
	{
		size_t  rdSize = fread(pReadBuff, 1, 1024, fIn); //从fIn文件中读取
		if (0 == rdSize)
		{
			break;
		}
		for (size_t i = 0; i < rdSize; ++i)
		{
			pos = 7;
			for (size_t j = 0; j < 8; ++j)  //处理当前的一个字节
			{
				//if ((pReadBuff[i] & (1 << pos)) == 1)//1朝右走
				if (pReadBuff[i] & (1 << pos))//1朝右走
					pCur = pCur->_pRight;
				else
					pCur = pCur->_pLeft;  //0朝左走
				if (nullptr == pCur->_pLeft && nullptr == pCur->_pRight)
				{
					fputc(pCur->_weight._ch, fOut);  //把这个字符写到fOut文件中
					pCur = ht.GetRoot();
					SizeOfFile--;
					if (0 == SizeOfFile)
					{
						break;
					}

				}
				pos--;
			}
		}
	}
	delete[] pReadBuff;
	fclose(fIn);
	fclose(fOut);
}

void FileCompressHuffmanM::GetHuffmanCode(HuffmanTreeNode<CharInfo>* pRoot)
{
	if (pRoot == nullptr)
	{
		return;
	}
	GetHuffmanCode(pRoot->_pLeft);
	GetHuffmanCode(pRoot->_pRight);
	if ((pRoot->_pLeft == nullptr) && (pRoot->_pRight == nullptr))
	{
		//找到叶子结点 ,保存叶子节点,并追寻双亲节点
		HuffmanTreeNode<CharInfo>* pCur = pRoot;  //以字符信息作为模板类型,pCur类型是个结构体
		HuffmanTreeNode<CharInfo>* pParent = pCur->_pParent;

		string& strCode = _charInfo[pCur->_weight._ch]._strCode; //?????数组是结构体类型,把编码保存在结构体的_strCode中
		while (pParent)            //以字符的ASCII作为数组的下标:   [叶子结点 中的权值 中的字符]
		{
			if (pCur == pParent->_pLeft)
			{
				strCode += '0';
			}
			else 
			{
				strCode += '1';
			}
			pCur = pParent;
			pParent = pCur->_pParent;
		}
		reverse(strCode.begin(), strCode.end());
	}
}

void FileCompressHuffmanM::WriteHead(FILE* fOut, const std::string& strFilePath)  //写入解压缩需要的信息
{
	string strHeadInfo;
	strHeadInfo = strFilePath.substr(strFilePath.rfind('.'));
	strHeadInfo += '\n';

	string strCharInfo;
	char szCount[32];
	size_t lineCount = 0;
	for (size_t i = 0; i < 256; ++i)
	{
		if (_charInfo[i]._charCount)
		{
			strCharInfo += _charInfo[i]._ch;   //把字符  和  次数放入  头信息中
			strCharInfo += ',';
			_itoa(_charInfo[i]._charCount, szCount, 10);
			strCharInfo += szCount;
			strCharInfo += '\n';
			lineCount++;
		}
	}
	_itoa(lineCount, szCount, 10);     //整型数据要转换成字符串
	strHeadInfo += szCount;
	strHeadInfo += '\n';

	strHeadInfo += strCharInfo;
	fwrite(strHeadInfo.c_str(), 1, strHeadInfo.size(), fOut);
}

void FileCompressHuffmanM::GetLine(FILE* fIn, std::string& strContent)
{
	while (!feof(fIn)) //指针没有在文件末尾,就读取一行
	{
		UCH ch = fgetc(fIn);
		if ('\n' == ch)
		{
			return;
		}
		strContent += ch;
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值