基于Huffman算法的文件压缩项目

目录

文件压缩的概念

为什么要压缩

压缩的分类

有损压缩

无损压缩

Huffman树

建立huffman树步骤如下:

Huffman编码

项目具体实现

创建Huffman树

HuffmanTree的节点实现

HuffmanTree构造函数的实现

Huffman树的销毁实现

创建ByteInfo类

创建FileCompressHuffman类

FileCompressHuffman类构造函数的实现

压缩文件接口的实现

a.统计源文件中字符出现的次数

 b.用统计的结果创建huffman树

c.生成Huffman编码

d.用来写解压缩时需要用的信息

e.用获取的编码对源文件进行改写,生成压缩文件

解压缩文件接口的实现

a.获取解压缩需要用到的信息

b.还原Huffman树

c.解压缩

项目测试代码实现

项目展示

项目完整代码


文件压缩的概念

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

简单来说,文件压缩就是利用某种算法,在不丢失源文件信息的前提下,对源文件进行替换的过程。

为什么要压缩

主要有三个方面:

1.紧缩数据存储容量减少存储空间

2.可以提高数据的传输速度;减少带宽占用量,提高通讯效率

3.对数据的一种加密保护增强传输过程中的安全性

压缩的分类

有损压缩

有损压缩是利用人类对图像或者声波中的某些频率成分不敏感的特性,允许压缩过程中损失一定的信息,虽然不能完全恢复原始数据,但是所损失的部分对理解原始图像的影响缩小,却换来了大得多的压缩比,即使使用压缩后的数据进行重构,重构后的数据与原来的数据有所不同,但不影响人对原始资料表达的信息造成误解。

其实在生活中,有损压缩是很常见的,比如说,源图片的分辨率是4k,但是被压缩成了2k和1.5k,但是人眼几乎是看不到这些差别的。看看我们的锤子哥:

无损压缩

无损压缩是对文件中数据按照特定的编码格式进行重新组织,压缩后的压缩文件可以被还原成与源文件完全相同的格式,不会影响文件内容,对于数码图像而言,不会使图像细节有任何损失。

我们本次的实现就是实现一个无损压缩的项目。

说了这么多,我们究竟怎样实现无损压缩这个项目呢?要实现这个项目我们不得不引入和Huffman编码的概念:

Huffman树

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。(以上内容来自百度百科)

建立huffman树步骤如下:

前提:由多个二叉树组成的森林,这些二叉树都只有一个根节点,且每一个节点都有对应的权值。

步骤1:从二叉树森林中选出权值最小的两个节点,这两个节点组成一个新的节点,新的节点的权值为这两个节点的权值之和,且新的节点为这两个节点的双亲,然后将这三个节点组成的二叉树再放入二叉树森林中。

步骤2:重复步骤1,直到这个二叉树森林中最终只剩下一个二叉树,那么这个二叉树就是一个Huffman树。

规律一:最终Huffman树的所有的叶子节点都是刚开始的森林中的节点,且刚开始的森林中的所有节点的权值之和相加之后等于最终Huffman树的根节点的权值。

记住这个规律,后续我们会用到。

Huffman编码

哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。(以上内容来自百度百科)

其实简单来说,huffman编码其实就是为huffman树的由根节点到叶子结点的所有路径赋上权值。我们也称这个路径为带权路径。

就拿上面那个Huffman树而言,如果当前节点存在双亲节点,且当前节点为双亲节点的左孩子,那么此节点的的路径权值就是0,否则就是1。图示如下:

上述Huffman树所有的叶子节点的带权路径为:

权值为4的叶子节点的带权路径为:00

权值为5的叶子节点的带权路径为:01

权值为7的叶子节点的带权路径为:10

权值为2的叶子节点的带权路径为:110

权值为3的叶子节点的带权路径为:111

规律二:权值大的节点的带权路径编码短,权值小的节点的带权路径编码长。

项目具体实现

具体思想:通过文件压缩原理和Huffman编码我们不难发现,因为文件压缩的本质其实就是编码替换,对于一个文件而言是由多个字符组成的,那么我们就可以让Huffman树的每个叶子节点为文件中出现的各种不同的字符,节点的权值就是每个字符出现的次数。出现次数多的权值大,也就对应的带权路径编码短,出现次数少的权值小,对应的带权路径编码长,其实这也符合我们的目的,用较短的编码替换出现次数多的,用较长的编码替换次数少的。

创建Huffman树

我们使用HuffmanTree类表示Huffman树,使用HuffanHTreeNode表示Huffman树节点。

HuffmanTree的节点实现

template<class W>
struct HuffmanTreeNode
{
	HuffmanTreeNode<W>* _left;
	HuffmanTreeNode<W>* _right;
	HuffmanTreeNode<W>* _parent;
	W _weight;

	//构造函数
	HuffmanTreeNode(const W& weight = W())
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _weight(weight)
	{

	}
};

 解析:我们使用了三叉链结构,存储双亲节点主要是为了将来挑选两个权值小的节点建新树时,建立链接关系,以及在下面我们用来获取带权路径的编码时从叶子节点开始往根节点遍历。

HuffmanTree构造函数的实现

HuffmanTree(const std::vector<W>& vw, const W& invalid)
	{
		//1.用所有的权值构造只有根节点的二叉树森林
		//森林中的二叉树应该使用堆(优先级队列)来保存
		//优先级队列默认是大堆,仿函数 
		std::priority_queue < Node*, std::vector<Node*>, Compare> q;

		for (auto& e : vw)
		{
			if (invalid != e)
			{
				q.push(new Node(e));
			}

		}

		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;
			left->_parent = parent;
			parent->_right = right;
			right->_parent = parent;

			//最后再将parent放入二叉树森林中

			q.push(parent);
		}
		_root = q.top();
	}

解析:在创建Huffman树时,我们每次都要从森林中找出权值最小的两个节点,基于这一特性,我么可以使用优先级队列来实现,因为优先级队列本质上就是堆,所以我么可以使用优先级队列来存储每个节点的地址。但是优先级队列本质上是一个大堆,所以我们得使用仿函数让优先级队列变成小堆。

template<class W>

class HuffmanTree
{
	typedef HuffmanTreeNode<W> Node;
	class Compare
	{
	public:
		bool operator()(const Node* x, const Node* y)
		{
			return x->_weight > y->_weight;
		}
	};

Huffman树的销毁实现

因为Huffman树的每个节点都是在堆上开辟的空间,为了防止内存泄漏,所以必须对其进行释放。

	~HuffmanTree()
	{
		Destroy(_root);
	}

private:
	void Destroy(Node*& root)
	{
		if (root)
		{
			Destroy(root->_left);
			Destroy(root->_right);
			delete root;
			root = nullptr;
		}
	}
	Node* _root;

 因为Huffman树的根节点是private类型,所以必须提供公共的接口,以便在类外进行访问:

	Node* GetRoot()
	{
		return _root;
	}

创建ByteInfo类

我们创建ByteInfo类的目的是,让ByteInfo对象表示一个字符相关的属性,具体为这个字符是什么,这个字符的出现此处,这个字符的替换码。

struct ByteInfo
{
	uch _ch;
	size_t _appearCount;
	string _chCode;


	ByteInfo(size_t appearCount = 0)
		:_appearCount(appearCount)
	{}

	ByteInfo operator+(const ByteInfo& other) const
	{
		return ByteInfo(_appearCount + other._appearCount);
	}

	bool operator>(const ByteInfo& other)const
	{

		return _appearCount > other._appearCount;
	}
	bool operator!=(const ByteInfo& other)const
	{

		return _appearCount != other._appearCount;
	}
	bool operator==(const ByteInfo& other)const
	{

		return _appearCount == other._appearCount;
	}
};

同时我们也提供了一个运算符重载函数,比如再进行权值的比较时,因为HuffmanTreeNode中的每个节点的权值的类型我们用了ByteInfo类,所以在进行权值的比较时,因为自定义类型无法进行比较,多以我们就创建了对应的运算符重载函数进行比较,当然还有一些其他的比较,必须比较权值是否相等。比如两个节点的权值进行相加,这些都是要对ByteInfo进行运算符重载,然后使用ByteInfo中的_appearCount来进行相应的运算操作。

创建FileCompressHuffman类

这个类是我们项目中的主类:


class FileCompressHuffman
{
public:
	FileCompressHuffman();
	void CompressFile(const string& filePath);
	void UNCompressFile(const string& filePath);

private:
	void WriteHeadInfo(const string& filePath, FILE* fOut);
	void GenerateHuffmanNode(HuffmanTreeNode<ByteInfo>* root);
	string GetFilePostFix(const string& filePath);
	void GetLine(FILE* fIn, string& strInfo);
	std::vector<ByteInfo> _fileInfo;
};

我们使用了_fileInfo作为成员变量,_fileInfo是一个vector对象,它的每一个元素都是ByteInfo类型,因为Huffman树使用权值来创建节点的,所以要传一个vector集合,集合的每个元素都是权值类型,在这里权值的类型就是ByeInfo类型所以这个_fileInfo使我们用来创建Huffman树的节点的。

FileCompressHuffman类构造函数的实现

FileCompressHuffman::FileCompressHuffman()
{
	_fileInfo.resize(256);
	for (int i = 0; i < 256; i++)
	{
		_fileInfo[i]._ch = i;

	}
}

 我们知道一个字符的大小是一个字节(8bit),所以也就存在2^8(256)种字符。一个字符我们用一个ByteInfo对象来表示,所以256个字符就需要用256个ByteInfo对象来表示,所以我们就需要为vector申请256个元素的空间,且我们进行赋值时,巧妙的让下标为i的元素所对应的ByteInfo对象存储的就是ASCII码为i的字符。所以后续我们就可以使用字符表示vector每个元素的下标,从而定位到该字符所对应的ByteInfo对象,然后就可以访问对象中的相关成员变量。

压缩文件接口的实现

a.统计源文件中字符出现的次数
    FILE* fIn = fopen(filePath.c_str(), "rb");
	if (nullptr == fIn)
	{
		cout << "待压缩的文件不存在" << endl;
		return;
	}
	uch rdBuff[1024];
	while (true)
	{
		size_t rdSize = fread(rdBuff, 1, 1024, fIn);
		if (0 == rdSize)
		{
			break;
		}

		for (size_t i = 0; i < rdSize; ++i)
		{
			_fileInfo[rdBuff[i]]._appearCount++;
		}
	}
 b.用统计的结果创建huffman树
HuffmanTree<ByteInfo> ht(_fileInfo, ByteInfo());

我们为什么要传第二个参数呢,这是因为我们在进行huffman树的创建时,我们只需要文件中出现过的字符,没有出现过的字符我们是没有必要把它加入到最初的二叉树森林中的。 所以我们就创建了一个匿名对象,它的出现次数是0,所以可以让_fileInfo中的每一个对象去与其对比。

c.生成Huffman编码
void FileCompressHuffman::GenerateHuffmanNode(HuffmanTreeNode<ByteInfo>* root)
{
	if (nullptr == root)
		return;
	GenerateHuffmanNode(root->_left);
	GenerateHuffmanNode(root->_right);

	//root就是叶子节点
	if (nullptr == root->_left && nullptr == root->_right)
	{
		string& chCode = _fileInfo[root->_weight._ch]._chCode;
		//string& chCode = root->_weight._chCode;
		HuffmanTreeNode<ByteInfo>* cur = root;
		HuffmanTreeNode<ByteInfo>* parent = cur->_parent;
		while (parent)
		{
			if (cur == parent->_left)
			{
				chCode += '0';

			}
			else
			{
				chCode += '1';
			}
			cur = parent;
			parent = cur->_parent;
		}
		reverse(chCode.begin(), chCode.end());
	}

}

 先通过递归的方法到达叶子节点。然后判断当前叶子节点是其双亲的左孩子还是右孩子,如果是左孩子,就将当前叶子节点的所对应的ByteInfo对象的_chCode+='0',否则就是1。然后通过cur和parent的互相赋值最终到达根节点,但是需要注意的是,这样子到达根节点之后,我们得到的带权路径编码时与真实的带权路径编码相反的,真正的应该是从根节点往下遍历一直遍历到根节点,所以我们得对最终得到的_chCode进行逆置。

d.用来写解压缩时需要用的信息

因为我们最终要解压缩还原生成源文件,所以就需要知道源文件的文件后缀,要还原huffman树,就需要知道源文件中每个出现的字符以及字符出现次数。

void  FileCompressHuffman::WriteHeadInfo(const string& filePath, FILE* fOut)
{
	//1.获取源文件后缀
	string headInfo;
	headInfo += GetFilePostFix(filePath);
	headInfo += '\n';
	//2.获取频次信息
	//统计多少行,其实也就是统计字符的种类数
	size_t appearLineCount = 0;
	//获取每一行具体的内容:如A:1,B:3,
	string chInfo;
	for (auto& e : _fileInfo)
	{
		if (0 == e._appearCount)
		{
			continue;
		}
		chInfo += e._ch;
		chInfo += ':';
		chInfo += std::to_string(e._appearCount);
		chInfo += '\n';
		appearLineCount++;
	}
	headInfo += std::to_string(appearLineCount);
	headInfo += '\n';
	fwrite(headInfo.c_str(), 1, headInfo.size(), fOut);
	fwrite(chInfo.c_str(), 1, chInfo.size(), fOut);

}
e.用获取的编码对源文件进行改写,生成压缩文件
//5.用获取的编码对源文件进行改写
	fseek(fIn, 0, SEEK_SET);

	uch bits = 0;
	int bitCount = 0;
	while(true)
	{
		size_t rdSize = fread(rdBuff, 1, 1024, fIn);
		if (rdSize == 0)
		{
			break;
		}

		for (size_t i = 0; i < rdSize; ++i)
		{
			string& strCode=_fileInfo[rdBuff[i]]._chCode;
			for (size_t j=0;j<strCode.size();++j)
			{
				bits <<= 1;
				if ('1' == strCode[j])
				{
					bits |= 1;
				}
				bitCount++;
				if (8 == bitCount)
				{
					fputc(bits, fout);
					bits = 0;
					bitCount = 0;
				}
			}
		}
	}
	//最后一次bits中的8个比特位可能没有填充满
	if (bitCount > 0 && bitCount < 8)
	{
		bits <<= (8 - bitCount);
		fputc(bits, fout);
	}

 在之前我们读取源文件时,读取完之后文件指针已经指向了文件末尾,所以此时我们要读取源文件就得将文件指针置于文件起始位置处。然后再通过读取缓冲区中的元素,得到每个字符所对应的_chCode码。然后因为_chCode是string类型,所以就需要遍历string类,将string类中的元素为'0'时,啥也不用管,但如果是'1',就得让我们创建的bits与1相或。在比较是‘0’还是‘1’之前,我们都得先将bits左移一位,这是为了产生‘0’这一位,而且为了避免后续的覆盖。

注意:最后一个bits的8位可能没有满,因为我么解压缩时,是从左往右依次遍历bits的每一位的,所以我们得将最后一个bits没满的位置,通过左移使其与上一个bits位的最后一位连接起来,方便后续进行解压缩。

解压缩文件接口的实现

a.获取解压缩需要用到的信息
//1.读取解压缩需要用的信息
	FILE* fIn = fopen(filePath.c_str(), "rb");
	if (nullptr == fIn)
	{
		cout << "解压缩文件路径不对" << endl;
		return;
	}
	//a.读取源文件后缀
	string unCompressFile("D:/学习/新建文件夹/333.");

	string strInfo;
	GetLine(fIn, strInfo);
	unCompressFile += strInfo;

	//b. 读取频次信息总行数
	strInfo = "";
	GetLine(fIn, strInfo);
	size_t lineCount = atoi(strInfo.c_str() );
	for (size_t i = 0; i < lineCount; ++i)
	{

		strInfo = "";
		GetLine(fIn, strInfo);
		//源文件中有'\n'需要注意,得进行特殊处理
		if ("" == strInfo)
		{ 
			strInfo += '\n';
			GetLine(fIn, strInfo);
		}
		//A:1
		uch ch = strInfo[0];	
		_fileInfo[ch]._appearCount = atoi(strInfo.c_str()+2);
	}

这里需要注意的是,我们在获取元素以及元素出现的次数时,我们使用GetLine这个函数实现的,这个函数遇到‘\n’会停止,但是如果我们原来的源文件中就有‘\n’这个字符,那么我们可以会读取不到这个字符,比如正常是\n:3,但是这种情况下我们读到的就可能是:3,这样就会导致最终在解压缩时出错。所以需要进行特殊处理。

b.还原Huffman树

	//2.还原huffman树
	HuffmanTree<ByteInfo>ht(_fileInfo,ByteInfo());
c.解压缩
	FILE* fOut = fopen(unCompressFile.c_str(), "wb");
    uch rdBuff[1024];
	HuffmanTreeNode<ByteInfo>* cur = ht.GetRoot();
	size_t fileSize = 0;
	while (true)
	{
		size_t rdSize = fread(rdBuff, 1, 1024, fIn);
		if (0 == rdSize)
			break;
		for (size_t i = 0; i < rdSize; ++i)
		{
			char ch = rdBuff[i];
			for (size_t j = 0; j < 8; ++j)
			{
				if (ch & 0x80)
				{
					cur = cur->_right;
				}
				else
				{
					cur = cur->_left;
				}
				ch <<= 1;
				if (nullptr == cur->_left && nullptr == cur->_right)
				{
					fputc(cur->_weight._ch, fOut);
					cur = ht.GetRoot();
					fileSize++;
					if (fileSize == cur->_weight._appearCount)
					break;
				}
			}
		}

	}

解析:解压缩的原理就是,先从压缩文件中读取替换的带权路径编码,然后进行解码,从根节点开始,如果如果第一个bit位是0,就向左遍历,如果是1,就向右遍历,一直到达叶子节点,此时叶子节点对应的ByteInfo对象中的_ch就是我们要还原的字符。但是需要注意的是,最后一个bit我们不用遍历完,我们要用到上述的规律一,huffman树根节点的权值就是所有叶子节点的权值之和,也是最初的二叉树森林中所有的节点的权值之和,所以根节点的权值就是所有所有字符出现的次数,所以可以用根节点的权值作为字符是否已经还原完毕的标志。

项目测试代码实现


void menu()
{
	cout << "*****************************************************************" << endl;
	cout << "*************************  0.退出           *********************" << endl;
	cout << "*************************  1.huffman压缩    *********************" << endl;
	cout << "*************************  2.huffman解压缩  *********************" << endl;
	cout << "*****************************************************************" << endl;
}

int main()
{
	int input = 0;
	bool isQuit = false;
	FileCompressHuffman fc;
	string fileName;
	while (true)
	{
		menu();
		cin >> input;
		switch (input)
		{
		case 0:
			isQuit = true;
			break;
		case 1:
	
			cout << "输入压缩文件的名称>";
			cin >> fileName;
			fc.CompressFile(fileName);
			break;
		case 2:
			cout << "输入解压缩文件的名称>";
			cin >> fileName;
			fc.UNCompressFile(fileName);
			break;
		}
		if (isQuit)
			break;
	}
	return 0;
}

我们使用了一个简单的菜单界面用来测试。

项目展示

未压缩前:

压缩之后:

解压之后:

总结:

再实现这个项目时需要注意三点:

1.因为char类型的字符对应的ASCII码可能会是负值,比如说是汉字,所以汉字在强转为ASCII码时不能作为下标,所以我们在整个项目中我们将char类型全部转化成了unsigned char类型,这样字符对应的ASCII码就全部转变成了正值,就可以作为数组下标。

2.我们在进行文件的打开时,全部是以二进制的形式打开的,不用文本形式打开的原因是,文本形式的文件打开之后,是以EOF(-1)作为文件结束的标志的,但是我们的压缩文件是二进制文件,如果以文本形式进行打开,最终在读取文件时,可能遇到了-1就提前结束了,并读取完全,导致解压缩出错,所以需要使用二进制方式进行读取。

3.文件中出现‘\n’字符时要进行特殊处理。

项目完整代码

HuffmanTree.hpp:

#include<iostream>
#include<vector>
#include<queue>
template<class W>
struct HuffmanTreeNode
{
	HuffmanTreeNode<W>* _left;
	HuffmanTreeNode<W>* _right;
	HuffmanTreeNode<W>* _parent;
	W _weight;

	//构造函数
	HuffmanTreeNode(const W& weight = W())
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _weight(weight)
	{

	}
};


template<class W>

class HuffmanTree
{
	typedef HuffmanTreeNode<W> Node;
	class Compare
	{
	public:
		bool operator()(const Node* x, const Node* y)
		{
			return x->_weight > y->_weight;
		}
	};

public:
	//构造函数
	HuffmanTree()
		: _root(nullptr)
	{
	}

	HuffmanTree(const std::vector<W>& vw, const W& invalid)
	{
		//1.用所有的权值构造只有根节点的二叉树森林
		//森林中的二叉树应该使用堆(优先级队列)来保存
		//优先级队列默认是大堆,仿函数 
		std::priority_queue < Node*, std::vector<Node*>, Compare> q;

		for (auto& e : vw)
		{
			if (invalid != e)
			{
				q.push(new Node(e));
			}

		}

		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;
			left->_parent = parent;
			parent->_right = right;
			right->_parent = parent;

			//最后再将parent放入二叉树森林中

			q.push(parent);
		}
		_root = q.top();
	}

	~HuffmanTree()
	{
		Destroy(_root);
	}
	Node* GetRoot()
	{
		return _root;
	}

private:
	void Destroy(Node*& root)
	{
		if (root)
		{
			Destroy(root->_left);
			Destroy(root->_right);
			delete root;
			root = nullptr;
		}
	}
	Node* _root;
};

FileCompressHaffman.h:

#include<string>
#include"HuffmanTree.hpp"
#include"Common.h"
using std::string;

struct ByteInfo
{
	uch _ch;
	size_t _appearCount;
	string _chCode;


	ByteInfo(size_t appearCount = 0)
		:_appearCount(appearCount)
	{}

	ByteInfo operator+(const ByteInfo& other) const
	{
		return ByteInfo(_appearCount + other._appearCount);
	}

	bool operator>(const ByteInfo& other)const
	{

		return _appearCount > other._appearCount;
	}
	bool operator!=(const ByteInfo& other)const
	{

		return _appearCount != other._appearCount;
	}
	bool operator==(const ByteInfo& other)const
	{

		return _appearCount == other._appearCount;
	}
};

class FileCompressHuffman
{
public:
	FileCompressHuffman();
	void CompressFile(const string& filePath);
	void UNCompressFile(const string& filePath);

private:
	void WriteHeadInfo(const string& filePath, FILE* fOut);
	void GenerateHuffmanNode(HuffmanTreeNode<ByteInfo>* root);
	string GetFilePostFix(const string& filePath);
	void GetLine(FILE* fIn, string& strInfo);
	std::vector<ByteInfo> _fileInfo;
};

FileCompressHuffman.cpp:

#define _CRT_SECURE_NO_WARNINGS
#include"FileCompressHaffman.h"
#include<algorithm>

FileCompressHuffman::FileCompressHuffman()
{
	_fileInfo.resize(256);
	for (int i = 0; i < 256; i++)
	{
		_fileInfo[i]._ch = i;

	}
}


void FileCompressHuffman::CompressFile(const string& filePath)
{
	//1.统计源文件中每个字符出现的次数
	FILE* fIn = fopen(filePath.c_str(), "rb");
	if (nullptr == fIn)
	{
		cout << "待压缩的文件不存在" << endl;
		return;
	}
	uch rdBuff[1024];
	while (true)
	{
		size_t rdSize = fread(rdBuff, 1, 1024, fIn);
		if (0 == rdSize)
		{
			break;
		}

		for (size_t i = 0; i < rdSize; ++i)
		{
			_fileInfo[rdBuff[i]]._appearCount++;
		}
	}
	//2.用统计的结果创建哈夫曼树

	HuffmanTree<ByteInfo> ht(_fileInfo, ByteInfo());

	//3.生成huffman 编码
	GenerateHuffmanNode(ht.GetRoot());
	FILE* fout = fopen("D:/学习/新建文件夹/222.hz", "wb");

	//4.写用来解压缩的数据信息
	WriteHeadInfo(filePath,fout);

	//5.用获取的编码对源文件进行改写
	fseek(fIn, 0, SEEK_SET);

	uch bits = 0;
	int bitCount = 0;
	while(true)
	{
		size_t rdSize = fread(rdBuff, 1, 1024, fIn);
		if (rdSize == 0)
		{
			break;
		}

		for (size_t i = 0; i < rdSize; ++i)
		{
			string& strCode=_fileInfo[rdBuff[i]]._chCode;
			for (size_t j=0;j<strCode.size();++j)
			{
				bits <<= 1;
				if ('1' == strCode[j])
				{
					bits |= 1;
				}
				bitCount++;
				if (8 == bitCount)
				{
					fputc(bits, fout);
					bits = 0;
					bitCount = 0;
				}
			}
		}
	}
	//最后一次bits中的8个比特位可能没有填充满
	if (bitCount > 0 && bitCount < 8)
	{
		bits <<= (8 - bitCount);
		fputc(bits, fout);
	}
	fclose(fIn);
	fclose(fout);
}

//3.获取huffman编码  
void FileCompressHuffman::GenerateHuffmanNode(HuffmanTreeNode<ByteInfo>* root)
{
	if (nullptr == root)
		return;
	GenerateHuffmanNode(root->_left);
	GenerateHuffmanNode(root->_right);

	//root就是叶子节点
	if (nullptr == root->_left && nullptr == root->_right)
	{
		string& chCode = _fileInfo[root->_weight._ch]._chCode;
		//string& chCode = root->_weight._chCode;
		HuffmanTreeNode<ByteInfo>* cur = root;
		HuffmanTreeNode<ByteInfo>* parent = cur->_parent;
		while (parent)
		{
			if (cur == parent->_left)
			{
				chCode += '0';

			}
			else
			{
				chCode += '1';
			}
			cur = parent;
			parent = cur->_parent;
		}
		reverse(chCode.begin(), chCode.end());
	}

}


void  FileCompressHuffman::WriteHeadInfo(const string& filePath, FILE* fOut)
{
	//1.获取源文件后缀
	string headInfo;
	headInfo += GetFilePostFix(filePath);
	headInfo += '\n';
	//2.获取频次信息
	//统计多少行,其实也就是统计字符的种类数
	size_t appearLineCount = 0;
	//获取每一行具体的内容:如A:1,B:3,
	string chInfo;
	for (auto& e : _fileInfo)
	{
		if (0 == e._appearCount)
		{
			continue;
		}
		chInfo += e._ch;
		chInfo += ':';
		chInfo += std::to_string(e._appearCount);
		chInfo += '\n';
		appearLineCount++;
	}
	headInfo += std::to_string(appearLineCount);
	headInfo += '\n';
	fwrite(headInfo.c_str(), 1, headInfo.size(), fOut);
	fwrite(chInfo.c_str(), 1, chInfo.size(), fOut);

}

string  FileCompressHuffman::GetFilePostFix(const string& filePath)
{
	return filePath.substr(filePath.find_last_of('.') + 1);
}


void FileCompressHuffman::UNCompressFile(const string& filePath)
{
	if (GetFilePostFix(filePath) != "hz")
	{
		cout << "压缩文件的格式不对"<<endl;
		return;
	}
	
	//1.读取解压缩需要用的信息
	FILE* fIn = fopen(filePath.c_str(), "rb");
	if (nullptr == fIn)
	{
		cout << "解压缩文件路径不对" << endl;
		return;
	}
	//a.读取源文件后缀
	string unCompressFile("D:/学习/新建文件夹/333.");

	string strInfo;
	GetLine(fIn, strInfo);
	unCompressFile += strInfo;

	//b. 读取频次信息总行数
	strInfo = "";
	GetLine(fIn, strInfo);
	size_t lineCount = atoi(strInfo.c_str() );
	for (size_t i = 0; i < lineCount; ++i)
	{

		strInfo = "";
		GetLine(fIn, strInfo);
		//源文件中有'\n'需要注意,得进行特殊处理
		if ("" == strInfo)
		{ 
			strInfo += '\n';
			GetLine(fIn, strInfo);
		}
		//A:1
		uch ch = strInfo[0];	
		_fileInfo[ch]._appearCount = atoi(strInfo.c_str()+2);
	}

	//2.还原huffman树
	HuffmanTree<ByteInfo>ht(_fileInfo,ByteInfo());


	//3.解压缩
	FILE* fOut = fopen(unCompressFile.c_str(), "wb");
    uch rdBuff[1024];
	HuffmanTreeNode<ByteInfo>* cur = ht.GetRoot();
	size_t fileSize = 0;
	while (true)
	{
		size_t rdSize = fread(rdBuff, 1, 1024, fIn);
		if (0 == rdSize)
			break;
		for (size_t i = 0; i < rdSize; ++i)
		{
			char ch = rdBuff[i];
			for (size_t j = 0; j < 8; ++j)
			{
				if (ch & 0x80)
				{
					cur = cur->_right;
				}
				else
				{
					cur = cur->_left;
				}
				ch <<= 1;
				if (nullptr == cur->_left && nullptr == cur->_right)
				{
					fputc(cur->_weight._ch, fOut);
					cur = ht.GetRoot();
					fileSize++;
					if (fileSize == cur->_weight._appearCount)
					break;
				}
			}
		}

	}
	fclose(fIn);
	fclose(fOut);

}


void FileCompressHuffman::GetLine(FILE* fIn, string& strInfo)
{
	while (!feof(fIn))
	{
		char ch = fgetc(fIn);
		if (ch == '\n')
			break;
		strInfo += ch;
	}
}

测试代码,bitZipTest.cpp:

#include"FileCompressHaffman.h"


void menu()
{
	cout << "*****************************************************************" << endl;
	cout << "*************************  0.退出           *********************" << endl;
	cout << "*************************  1.huffman压缩    *********************" << endl;
	cout << "*************************  2.huffman解压缩  *********************" << endl;
	cout << "*****************************************************************" << endl;
}

int main()
{
	int input = 0;
	bool isQuit = false;
	FileCompressHuffman fc;
	string fileName;
	while (true)
	{
		menu();
		cin >> input;
		switch (input)
		{
		case 0:
			isQuit = true;
			break;
		case 1:
	
			cout << "输入压缩文件的名称>";
			cin >> fileName;
			fc.CompressFile(fileName);
			break;
		case 2:
			cout << "输入解压缩文件的名称>";
			cin >> fileName;
			fc.UNCompressFile(fileName);
			break;
		}
		if (isQuit)
			break;
	}
	return 0;
}

以上便是基于Huffman算法的文件压缩项目的全部内容^_^ 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

棠~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值