基于Huffman树的文件压缩

1、文件压缩

1、什么是文件压缩?

    在不丢失有用信息的前提下,缩减数据量以减少存储空间,提高其传输、存储和
 处理效率,或按照一定的算法对数据进行重新组织,减少数据的冗余和存储的空 间
 的一种技术方法。通俗来说,就是想办法,让文件变得更小(可以还原)

2、问什么要对文件进行压缩?

文件太大,节省空间
提高数据在网络上的传输效率
对数据保护---加密

3、文件压缩分类

无损压缩

源文件被压缩后,通过解压缩,可以还原为与源文件相同格式。

有损压缩

源文件被压缩后,通过解压缩,不能还原为与源文件相同格式。
解压缩文件时,对识别文件内容无影响。

2、Huffman压缩的引入

字节层面可能也有大量重复的。比如:"BCDCDDBDDCADCBDC"
一个字节占8个比特位,那如果能对所有字节找到小于8个比特位的编码,
然后用找到的编码对源文件中对应。

思考:字节重新进行改写,也可以让源文件更小。那如何找编码呢?

1、静态等长编码

每个字符的编码长度都相等,比如:
在这里插入图片描述

用等长编码对上述源数据进行压缩:01101110 11110111 11100011 10011110,
压缩完成后的结果只占4个字节,压缩率还是比较高的。

2、动态不等长编码
每个字符的编码根据具体的字符情况来确定,比如:

在这里插入图片描述

使用不等长编码对源数据进行压缩:10111011 00101001 11000111 01011
压缩完成后最后一个字节没有用完,还剩余3个比特位,

显然动态不等长编码比等长编码压缩率能好点。

问题那动态不等长编码如何获取到呢?

3、 huffman编码

1. haffman树

从二叉树的根结点到二叉树中所有叶结点的路径长度与相应权值的乘积之和
为该二叉树的带权路径长度WPL。

把带权路径最小的二叉树称为Huffman树。

2、huffman的创建

  1. 由给定的n个权值{ w1, w2, w3, … , wn}构造n棵只有根节点的二叉树森林F={T1, T2 , T3, … ,Tn},
    每棵二叉树Ti只有一个带权值wi的根节点,左右孩子均为空。

  2. 重复以下步骤,直到F中只剩下一棵树为止
    在F中选取两棵根节点权值最小的二叉树,作为左右子树构造一棵新的二叉树,新二叉树根节点的权值为其左右子树根节点的权值之和在F中删除这两棵二叉树把新的二叉树加入到F中。

3. 获取haffman编码

在这里插入图片描述
1、每个字符出现的总次数为权值,构建Huffman树
2、对Huffman树左分支用0代替,右分支用1代替
3、所有权值节点都在叶子位置,遍历每条叶子节点的路径,获取字符编码

A:100
B:101
C:11
D:0

因为Huffman树权值节点都在叶子结点的位置,路径唯一,
从而保证了在字符压缩时,不会出现冲突。

问题:如何找到权值最小的构建二叉树呢?

(1) 排序 (可以实现,效率太低)

采用vector保存,权值最小的排在前面。
Huffman树的构建过程相当于头删

在这里插入图片描述
(2)采用堆(priority_queue)保存
在这里插入图片描述
4、利用huffman编码对源文件进行压缩

1. 统计源文件中每个字符出现的次数
2. 以字符出现的次数为权值创建huffman树
3. 通过huffman树获取每个字符对应的huffman编码
4. 读取源文件,对源文件中的每个字符使用获取的huffman编码进行改写,
   将改写结果写到压缩文件中,直到文件结束

5、 解压缩

  1. 从压缩文件中获取源文件的后缀
  2. 从压缩文件中获取字符次数的总行数
  3. 获取每个字符出现的次数
  4. 重建huffman树
  5. 解压缩

Huffman.hpp

#pragma once
#pragma warning(disable:4996)
#include <iostream>
//using namespace std;
#include <queue>
#include <vector>
template<class W>   //W 代表节点的权值类型
class HuffManTreeNode
{
public:
	HuffManTreeNode(const W& weight =W()) // 构造,创建HuffMan结点
		:_pLeft(nullptr)
		,_pRight(nullptr)
		, _pParent(nullptr)
		, _weight(weight)
	{

	}

	HuffManTreeNode<W>* _pLeft; // 左指针
	HuffManTreeNode<W>* _pRight;// 右指针
	HuffManTreeNode<W>* _pParent;// 双亲指针
	W _weight; // 权值


};

template <class W>
class Greater
{
	typedef HuffManTreeNode<W> Node;
public:
	Greater()
	{}

	bool operator()(const Node* pLeft, const Node* pRight)
	{
		return pLeft->_weight > pRight->_weight; // 自定义类型需要重载
	}
};

template<class W>
class HuffmanTree // 创建HuffMan树
{
	typedef HuffManTreeNode<W> Node;   // 重命名

public:
	HuffmanTree()
		:_pRoot(nullptr)
	{

	}

	HuffmanTree(const std::vector<W> vweight, const W& invalid_weight) // 传入权值数组
		:_pRoot(nullptr)
	{
		CreateHuffManTree(vweight, invalid_weight);
	}

	~HuffmanTree()
	{
		_DestroyTree(_pRoot); // 销毁Huffman树

	}
	void CreateHuffManTree(const std::vector<W>& vWeight ,const W& invalid_weight) // 传入 权值, 无效权值invalid_weight , 过滤传入权值为0得
	{
		// (1). 构建森林

		// 1、采用优先级队列,存放结点,而优先级队列,只需要存放这些结点的地址就行

		std::priority_queue<Node*, std::vector<Node*>, Greater<W>> pq; // 小堆
		for (auto e : vWeight) //
		{
			if (e == invalid_weight) // 若传入为无效权值,跳过
				continue;
			//Node(e)将权值存入结点,new 返回地址,将结点地址压入优先队列
			pq.push(new Node(e)); //优先对列,存放节点的地址
		}

		while (pq.size()>1)  //优先队列中存放超过1个结点地址
		{
			Node* pLeft = pq.top(); // 堆顶最小权值结点地址
			pq.pop();

			Node* pRight = pq.top();// 优先队列更新后,堆顶最小权值结点地址
			pq.pop();


			// 将 用所取出两个结点地址,获取权值,
			// 创建新的结点,将两个结点的权值之和,存在在 新结点中
			Node* pParent = new Node(pLeft->_weight + pRight->_weight);  // 注意:_weight 得 类型为 W ,可以时自定义类型或者内置类型
			
			// 并将新节点地址作为双亲结点,
			pParent->_pLeft = pLeft;
			pParent->_pRight = pRight;


			pLeft->_pParent = pParent; // 左子树的双亲
			pRight->_pParent = pParent;// 右子树的双亲
			// 将新地址节点压入
			pq.push(pParent);
		}

		//跳出while 循环,此时就只剩一棵树 , 即 Huffman树  -- 构建成功
		_pRoot = pq.top();


	}
	
	Node* GetRoot()
	{
		return _pRoot;
	}

	void _DestroyTree(Node*& pRoot)//pRoot 为 _pRoot堆顶 的一份拷贝,因此需要用二级指针 或者 引用 才能将 栈顶_pRoot置空
	{
		if (pRoot) // 加入堆顶指针不为空,存在
		{
			// 递归销毁
			_DestroyTree(pRoot->_pLeft); //先消除左子树
			_DestroyTree(pRoot->_pRight); //在消除右子树
			delete pRoot;
			pRoot = nullptr;
		}
	}
private:
	Node* _pRoot;  //堆顶 

};



Huffman.hpp

#pragma once
#pragma warning(disable:4996)
// 基于Huffman的压缩
#include <string>
#include <assert.h>
#include <algorithm>
#include <functional> 
#include "Huffman.hpp"

// 统计源文件字符出现的次数
struct CharInfo
{

	unsigned char _ch; // 具体的字符
	size_t _count; // 字符出现的次数
	std::string _strCode; // 字符的Huffman编码

	CharInfo(size_t count = 0)
		:_count(count)
	{
	}

	CharInfo operator+(const CharInfo& ch)const // 重载两个对象相加 , 返回值也必须为对象
	{
		// 不能返回return ch._count + _count;
		//    
		return CharInfo(ch._count + _count); // 返回一个无名结构体对象
	}

	bool operator>(const CharInfo& ch)const 
	{
		return _count >ch._count; // 返回一个无名结构体对象
	}

	bool operator==(const CharInfo& ch)const //
	{
		return _count == ch._count; // 只需要判断次数,过滤掉次数为0的
	}
};

class FileCompressHuffman // 基于Huffman的压缩
{

public:
	FileCompressHuffman(); // 初始化统计字符数组

	void CompressFile(const std::string& path); //压缩文件的路径
	
	void UNCompressFile(const std::string& path); //解压文件存放路径

	
	
private:
	void GenerateHuffmanCode(HuffManTreeNode<CharInfo>* pRoot);// 获取每个字符的编码
	void WriteHead(FILE* fOut, const std::string& fileName); // 对压缩文件添加头部信息(文件后缀,编码行数,字符次数) ,filePostFix 文件后缀
	std::string GetFilePostFix(const std::string&  fileName); //获取文件名 

	void ReadLine(FILE* fIn, std::string& strInfo); // 读取一行字符串
private:
	std::vector<CharInfo> _fileInfo; // 结构体数组  统计保存 下标对应 字符种类,及各种字符数量

};

Huffman.cpp

#include "FileCompressHuff.h"



FileCompressHuffman::FileCompressHuffman() // 构造函数,初始化统计字符数组
{
	_fileInfo.resize(256);
	for (int i = 0; i < 256; ++i)
	{
		_fileInfo[i]._ch = i;
		_fileInfo[i]._count = 0;
	}

}


void FileCompressHuffman::CompressFile(const std::string& path)//压缩文件路径
{
	// 1.统计源文件字符出现的次数

			// 1.1 打开文件
	FILE* fIn = fopen(path.c_str(),"rb");// 

	if (nullptr == fIn)
	{
		assert(false);// 文件打开失败
		return;
	}	
			//1.2 文件打开成功
	//注意 char -1289 ---> 127   无法保存 256个字符
	unsigned char* pReadBuff = new  unsigned char[1024]; //开辟1024字节缓存区,供每次读取1k数据

	// 读取数据保存至pReadBuff,一个字符1字节,每次读取1024个,从fIn文件指针中读取;
	int readSize = 0;
	while (true)
	{
		readSize = fread(pReadBuff, 1, 1024, fIn);

		if (0 == readSize) // 如果读取到的字符为0,则已经读完了
			break;

		for (int i = 0; i < readSize; ++i)
		{
			//pReadBuff[i] --- 缓冲区第i个字符
			_fileInfo[pReadBuff[i]]._count++; // 缓冲区第i个字符,对应得下标

		}

		
	}

	fseek(fIn, 0, SEEK_SET); // 将fIn文件指针,移到起始读的位置,后面改写

	//2、以字符出现的次数为权值创建huffman树
	HuffmanTree<CharInfo> t(_fileInfo, CharInfo()); //_fileInfo 记录字符数量得 vector

	//3、获取每个字符的编码

	GenerateHuffmanCode(t.GetRoot());

	//4、用获取到的编码重新改写源文件

	FILE* fOut = fopen("2.txt","wb"); // 以只写形式,写出改写后的文件
	if (nullptr == fOut)
	{
		assert(false);
		return;
	}
	//  4.1  先向文件中添加 文件头  (文件名 , 行数 ,次数)等信息

	WriteHead(fOut,path); //path文件路径,


	//  4.2 添加编码

	char ch = 0; // 替换变量
	int bitCount = 0; // 比特位计数
	while (true)
	{
		readSize = fread(pReadBuff,1,1024,fIn);
		if ( 0 == readSize)
		{
			break;
		}

		
		//4.1 根据字符编码对读取到的内容进行重写 , 用Huffman编码进行字符替换
		for (size_t i = 0; i < readSize; ++i)
		{
			std::string strCode = _fileInfo[pReadBuff[i]]._strCode ; // 用Huffman编码进行字符替换


			for (size_t j = 0; j < strCode.size(); ++j)
			{
				ch <<= 1; // 只有 ++ -- 改变原变量

				if ('1' == strCode[j])
					ch |= 1;
				
				bitCount++;

				if (8 == bitCount)  // 最后一次,可能不够8个比特位
				{
					fputc(ch, fOut);   //将 字节 写入fOu->文件
					bitCount = 0;
					ch = 0;
				}

			}
		}


	}

	if (bitCount < 8)
	{
		ch << (8 - bitCount);  //因为是左移,所有不足8位,要将后面的往前移到第一个bit位
		fputc(ch,fOut);
	}

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


void FileCompressHuffman::GenerateHuffmanCode(HuffManTreeNode<CharInfo>* pRoot)
{
	if (pRoot == nullptr)
		return;

	GenerateHuffmanCode(pRoot->_pLeft);// 递归到叶子节点
	GenerateHuffmanCode(pRoot->_pRight);

	if (nullptr == pRoot->_pLeft && nullptr == pRoot->_pRight) // 叶子结点
	{
		std::string strCode;
		HuffManTreeNode<CharInfo>* pCur = pRoot; // 保存叶子节点
		HuffManTreeNode<CharInfo>* pParent = pRoot->_pParent; // 保存叶子节点双亲

		while (pParent)
		{
			if (pParent->_pLeft == pCur)
			{
				strCode += '0'; //
			}
			else
			{
				strCode += '1';
			}

			pCur = pParent;
			pParent = pCur->_pParent;
		}

		reverse(strCode.begin(),strCode.end());

		_fileInfo[pRoot->_weight._ch]._strCode = strCode;
	}


}

// 2.txt
//f:\123\2.txt
std::string FileCompressHuffman::GetFilePostFix(const std::string&  fileName) //获取文件名 
{

	return fileName.substr(fileName.rfind('.')); // 截取文件名 .文件类型
}

void  FileCompressHuffman::WriteHead(FILE* fOut, const std::string& fileName)  对压缩文件添加头部信息(文件后缀,编码行数,字符次数)
{

	assert(fOut);

	std::string  strHead;
	strHead += GetFilePostFix( fileName);;  // (1)向文件头添加后缀
	strHead += '\n';
	// 1、写文件的后缀
	//fwrite(filePostFix.c_str(), 1, filePostFix.size(), fOut);  //将 filePostFix.c_str() 通过fOut写入文件,每次写1字节

	// 写行数
	size_t lineCount = 0; // huffman编码的行数,// 统计待写入的  行数
	std::string strChCount; //记录各个字符及出现的次数,换行信息,// 统计待写入的  次数



	char szValue[32] = { 0 };
	for (int i = 0; i < 256;++i) // 统计每个出现不为0次数的字符,及其次数
	{
		CharInfo& charInfo = _fileInfo[i];
		if (_fileInfo[i]._count) //_fileInfo[i]._count 为 size_t类型
		{
			lineCount++;  // 统计待写入的  行数

			strChCount += charInfo._ch; // 对应字符

			strChCount += ':'; // 字符与字符次数之间用冒号区分

			//itoa(charInfo._count,szValue,10); // 将charInfo._count转化为字符类型,存入字符串strChCount中
			strChCount += _itoa(charInfo._count, szValue, 10); // 字符出现次数 , 需要将size_t 转化为 string ,存入字符串中
			strChCount += '\n'; // 换行


		}
	}

	
	//2、 将行数转化为 字符型 , 写入行数
	// itoa(lineCount,szValue,10); // 将行数转化为 字符型

	strHead += _itoa(lineCount, szValue, 10); // (2)向文件头 添加  行数
	strHead += '\n'; 

	strHead += strChCount; //(3) 向文件头添加 字符次数信息
	//将头写入文件
	fwrite(strHead.c_str() , 1 , strHead.size(),fOut);

	// 写入字符次数信息
}







//解压文件

void FileCompressHuffman::ReadLine(FILE* fIn, std::string& strInfo) // 读取一行字符串
{
	assert(fIn);

	while (!feof(fIn)) // 只要fIn指针没有到文件末尾
	{

		char ch = fgetc(fIn);//文件中读取一个字符,ch接收
		if (ch == '\n')// \n 为一行
		{
			break;
		}

		strInfo += ch;
	}
	
}







void  FileCompressHuffman::UNCompressFile(const std::string& path) //解压文件存放路径
{

	FILE* fIn = fopen(path.c_str(), "rb");  // 以读的方式打开压缩文件
	if (nullptr == fIn)
	{
		assert(false);
		return;
	}

///1、 文件头部

	/// 1.1 文件后缀
	
	std::string strFilePostFix; // 保存文件后缀
	ReadLine(fIn, strFilePostFix); // 通过一行一行的读取头部信息 , 不能使用getline() --- 文件流对象,不能使用文件指针; 



	//  1.2字符信息的总行数
	std::string strCount;
	ReadLine(fIn,strCount);
	size_t lineCount = atoi(strCount.c_str()); // 将字符转换为数字

	//1.3字符信息
	// A:1
	// B:3
	// C:5
	// D:7

	for (int i = 0; i < lineCount; ++i)
	{
		std::string strchCount; // 字符信息
		ReadLine(fIn, strchCount); // 读取一行字符信息
		
		
		if (strchCount.empty())
		{
			strchCount += '\n';
			ReadLine(fIn,strchCount);
		}
		// A:1
		// B:3
		// C:5
		// D:7

		//_fileInfo[strchCount[0]]; //  strchCount[0] --- 字符种类
		//strchCount需要选转化为c字符; //  strchCount.c_str()[2]--- 字符对应出现数目  -- 需要转化为整型
	
		_fileInfo[(unsigned char)strchCount[0]]._count = atoi(strchCount.c_str() + 2);
		//char testAA = _fileInfo[strchCount[0]]._ch;
		//int testCount = _fileInfo[strchCount[0]]._count;

		

	}

	// 还原Huffman树
	HuffmanTree<CharInfo> t;
	t.CreateHuffManTree(_fileInfo,CharInfo());

	FILE* fOut = fopen("3.txt","w"); // 写
	assert(fOut);
///2、对压缩后的Huffman还原

	char* pReadBuff = new char[1024];
	unsigned char ch = 0;

	HuffManTreeNode<CharInfo>* pCur = t.GetRoot();

	size_t fileSize = pCur->_weight._count; // 文件大小
	size_t unCount = 0;// 解压字符数
	while (true)
	{
		size_t readSize = fread(pReadBuff,1,1024,fIn);
		
		if (0 == readSize)
		{
			break;
		}
		
		for (size_t i = 0; i < readSize; ++i)
		{
	
			ch = pReadBuff[i];
			// 只需要将一个字节中的8个比特位单独处理

			for (int pos = 0; pos < 8; ++pos)
			{
				if (nullptr == pCur->_pLeft && nullptr == pCur->_pRight) // 到达叶子节点
				{

					//unCount++;
					fputc(pCur->_weight._ch, fOut);// 将字符写入到fOut所指的文件中
					if (unCount == fileSize)
						break;

					pCur = t.GetRoot(); // pCur 回到根的位置
				}
				//80 ----二进制 100000000
				if (ch & 0X80)  // 如果为 1 , 右子树
				{
					pCur = pCur->_pRight;
				}
				else // 如果为 0 , 左子树
				{
					pCur = pCur->_pLeft;
				}

				ch <<= 1; // 每次左移一位
				if (nullptr == pCur->_pLeft && nullptr == pCur->_pRight) // 到达叶子节点
				{
					
					unCount++;
					fputc(pCur->_weight._ch, fOut);// 将字符写入到fOut所指的文件中
					if (unCount == fileSize)
						break;

					pCur = t.GetRoot(); // pCur 回到根的位置
				}
			}
		}
	}

	delete[] pReadBuff;
	fclose(fIn);
	fclose(fOut);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
综合实验: 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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值