huffman树的应用——文件压缩

简述:利用哈夫曼编码进行文件的压缩和解压缩。

开发环境:windows,VS2013,C++

项目特点:

         压缩文件:读取文件中的字符,将其转化为哈弗曼编码,再通过位转化为压缩文件。                                    

         解压缩文件:从配置文件中读取字符及对应字符的出现次数建立哈夫曼树,得到解压缩文件中的字符。

         写配置文件,可使压缩和解压缩通过读取配置文件进行。总字符数为树的根结点的权重,不需要写入配置文件。

         对于大文件的处理,读取文件用二进制读取,接口会读成-1(EOF),结束的标志为FEOF。

先建立哈弗曼树,关于哈夫曼树:

Huffman树,又称为最优二叉树,是加权路径长度最短的二叉树。

【贪心算法】是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解。使用贪心算法构建Huffman树

下面看一个简单的描述



为了方便解压缩,我们需要编写配置文件

配置文件的格式为(字符,字符出现的次数,字符的huffman编码)



下面看下代码的实现

“test.cpp

#define _CRT_SECURE_NO_WARNINGS 1

#include "FileCompress.h"
#include<windows.h>

int main()
{
	int start =  GetTickCount();
	HuffmanFileCompress fc;
	fc.CompressFile("test.txt");
	HuffmanFileCompress fc2;
	fc2.UnCompressFile("test.txt.Compress");
	int end = GetTickCount();
	cout<<"压缩文件耗时:"<<end - start<<endl;

	system("pause");
	return 0;
}

“Heap.h”

#pragma once
#include <vector>
#include <assert.h>

template<class T>  
struct Less  
{  
	bool operator()(const T& left,const T& Right)  
	{  
		return left < Right;  
	}  
};  

template<class T>  
struct Greater  
{  
	bool operator()(const T& left,const T& Right)  
	{  
		return left > Right;  
	}  
};  

template<class T,class Compare = Greater<T>>//缺省值给大堆  
class Heap  
{  
public:  
	Heap()
	{}
	Heap(const T* arr,const size_t size)  
	{  
		for (int i = 0;i < size;i++)  
		{  
			_arr.push_back(arr[i]);  
		}  

		//建堆  
		for (int i = (_arr.size() - 2) / 2;i >= 0;i--)  
		{  
			//寻找非叶子结点,向下调整;  
			AdjustDown(i);  
		}  
	}  
	void Push(const T& data)  
	{  
		_arr.push_back(data);  
		AdjustUp(_arr.size()-1);  
	}  
	void Pop()  
	{  
		assert(_arr.empty() != true);  
		swap(_arr[0],_arr[_arr.size()-1]);  
		_arr.pop_back();  
		AdjustDown(0);  
	}  
	const T& Top()  
	{  
		return _arr[0];  
	}  
	bool Empty()  
	{  
		return _arr.empty();  
	}  
	int Size()  
	{  
		return _arr.size();  
	}  
	void Cout()  
	{  
		for (int i = 0; i < _arr.size();i++)  
		{  
			cout<<_arr[i]<<" ";  
		}  
		cout<<endl;  
	}  
private:  
	void AdjustDown(int parent)  
	{  
		int child = 2 * parent + 1;//左孩子  
		Compare com;  
		while (child < _arr.size())  
		{  
			if (((child+1) < _arr.size())&&(com(_arr[child + 1],_arr[child])))  
			{  
				++child;  
			}  
			if (com(_arr[child],_arr[parent]))  
			{  
				swap(_arr[child],_arr[parent]);  
				parent = child;  
				child = parent * 2 + 1;  
			}   
			else  
			{  
				break;  
			}  
		}  
	}  
	void AdjustUp(int child)  
	{  
		int parent = (child - 1) / 2;  
		Compare com;  
		while (child > 0)  
		{  
			if (com(_arr[child],_arr[parent]))  
			{  
				swap(_arr[child],_arr[parent]);  
				child = parent;  
				parent = (child - 1) / 2;  
			}   
			else  
			{  
				break;  
			}  
		}  
	}  
private:
	vector<T> _arr;  
};  

“Huffman.h”

#pragma once
#include <iostream>
using namespace std;
#include "Heap.h"
#include <assert.h>

template<class T>
struct HuffmanTreeNode
{
	T _weight;//权重
	HuffmanTreeNode<T>* _left;//指向左子树的指针
	HuffmanTreeNode<T>* _right;//指向右子树的指针
	
	HuffmanTreeNode(const T& weight = T())
		:_weight(weight)
		,_left(NULL)
		,_right(NULL)
	{}
};//存放HuffmanTree节点的数据结构

template<class T>
class HuffmanTree
{
	typedef HuffmanTreeNode<T> Node;
public:
	//构造函数
	HuffmanTree()
		:_root(NULL)
	{}
	//构造函数
	//构建huffmanTree
	HuffmanTree(T* arr, size_t size, const T& invalid = T())
	{
		//每次都选取最小的两个节点,用最小堆最为合适
		struct Less
		{
			bool operator()(Node* left, Node* right)
			{
				assert(left);
				assert(right);

				return left->_weight < right->_weight;
			}
		};

		//将这组数据建立成最小堆,堆中的每个元素的类型都是Node*,这是为了保存后面的父节点
		Heap<Node*, Less> MinHeap;//小堆
		for (size_t i = 0;i < size;i++)
		{
			//如果字符出现的次数不为0,那么就将他加入最小堆中
			if (arr[i]._count != invalid._count)
			{
				Node* node = new Node(arr[i]);
				MinHeap.Push(node);
			}
		}

		//运用Huffman算法,从堆中取出两个最小的节点构建新的父节点,并在小堆中将这两个节点删除,再把新构建的父节点插入小堆中继续重复此步骤
		Node* frist = NULL;
		Node* second = NULL;
		Node* parent = NULL;
		while (MinHeap.Size() > 1)
		{
			//选取两个最小的节点
			frist = MinHeap.Top();
			MinHeap.Pop();
			second = MinHeap.Top();
			MinHeap.Pop();

			//利用两个小节点构建父节点
			parent = new Node(frist->_weight + second->_weight);

			//指针指向要明确
			parent->_left = frist;
			parent->_right = second;

			MinHeap.Push(parent);
		}

		//小堆里面的最后一个节点就是HuffmanTree的根节点
		_root = MinHeap.Top();
	}
	//析构函数
	~HuffmanTree()
	{
		if (_root != NULL)
		{
			//销毁函数
			_Destory(_root);
		}
	}
public:
	//获取huffmanTree
	Node* GetRoot()
	{
		return _root;
	}
protected:
	void _Destory(Node* root)
	{
		if (NULL == root)
		{
			return;
		}

		_Destory(root->_left);
		_Destory(root->_right);
		delete root;
		root = NULL;
	}
private:
	Node* _root;
};//构件huffmanTree的数据结构

“FileCompress.h”

#pragma once
#include <iostream>
using namespace std;
#include<string>

#include "Huffman.h"

typedef long long LongType;

struct CharInfo
{
	unsigned char _ch;//保存字符
	LongType _count;//保存字符出现的次数
	string _code;//保存Huffman编码

	CharInfo(const LongType& count = 0)
		:_count(count)
	{}

	CharInfo operator+(CharInfo& ch)
	{
		return CharInfo(_count + ch._count);
	}

	bool operator<(CharInfo& ch)
	{
		return _count < ch._count;
	}
};

class HuffmanFileCompress
{
	typedef HuffmanTreeNode<CharInfo> Node;
public:
	HuffmanFileCompress()
	{
		//初始化每个位置的_data值
		for (int i = 0;i < 256;i++)
		{
			_infos[i]._ch = i;
		}
	}
public:
	//压缩文件
	void CompressFile(const char* filename)
	{
		FILE* fread = fopen(filename, "rb");//已只读形式打开文件
		if (NULL == fread)
		{
			cout<<"The File Open Fail"<<endl;
			exit(0);
		}

		//统计文件中字符出现的次数
		int ch = fgetc(fread);
		while (ch != EOF)//一直读到文件末尾
		{
			_infos[ch]._count++;
			ch = fgetc(fread);
		}

		//构建HuffmanTree
		CharInfo invalid;
		HuffmanTree<CharInfo> hufftree(_infos, 256, invalid);
		Node* root = hufftree.GetRoot();

		//获取huffman编码
		string code;
		_GetHuffmanCode(root, code);

		//将文件指针移到文件头
		fseek(fread, 0, SEEK_SET);
		string write(filename);//write = "filename"
		write += ".Compress";//压缩文件的名字

		//创建一个压缩文件,存放压缩文件的信息
		FILE* fwite = fopen(write.c_str(), "wb");

		ch = fgetc(fread);
		unsigned char data = 0;//压缩数据以二进制的形式存储在文件中
		int pos = 7;//控制bit位的移动次数

		while (ch != EOF)//读到文件结尾
		{
			const char* ptr = _infos[ch]._code.c_str();

			//遍历保存结点编码
			while (*ptr)
			{
				if (pos >= 0)
				{
					data = data | ((*ptr - '0')<< pos);
					--pos;
				}
				if (pos < 0)
				{
					fputc(data, fwite);
					pos = 7;
					data = 0;
				}

				ptr++;
			}
			ch = fgetc(fread);
		}

		//最后一个字符不管写没写满都要放进去
		fputc(data, fwite);

		//写配置文件,用于解压缩
		_WriteConfig(filename);

		fclose(fread);
		fclose(fwite);
		cout<<"压缩成功"<<endl;
	}
	//解压缩文件
	void UnCompressFile(const char* filename)
	{
		//文件必须存在,要不然解压什么
		assert(filename);

		string write(filename);
		//去掉压缩文件的后缀,加上配置文件的后缀,然后读取配置文件
		unsigned int index = write.rfind('.', write.size());
		write = write.substr(0, index);
		string writeconfig = write;
		writeconfig += ".config";

		//读取配置文件,将配置文件里面的信息添加到RInfo数组中。
		CharInfo RInfo[256];
		_ReadConfigFile(writeconfig.c_str(), RInfo);

		//解压缩文件
		write += ".UnCompress";
		FILE* fwrite = fopen(write.c_str(), "wb");

		CharInfo invalid;
		HuffmanTree<CharInfo> hft(RInfo, 256, invalid);
		Node* root = hft.GetRoot();
		if (NULL == root)
		{
			return ;
		}
		Node* cur = root;
		LongType count = (root->_weight)._count;

		//开始解压缩
		FILE* fread = fopen(filename, "rb");
		unsigned char ch = fgetc(fread);

		//用字符的总数来控制循环条件
		int pos = 8;
		while (count)
		{
			--pos;
			unsigned char val = 1;

			//需要对压缩文件一个字节一个字节的访问
			if (ch & (val << pos))
			{
				cur = cur->_right;
			} 
			else
			{
				cur = cur->_left;
			}

			//读到叶子结点说明已经找到一个字符
			if (cur->_left == NULL && cur->_right == NULL)
			{
				//如果读到叶子结点,那么就要把相应的字符写进解压缩文件中
				fputc(cur->_weight._ch, fwrite);

				//每次都要将cur重新设置为根节点
				cur = root;
				if (--count == 0)
				{
					break;
				}
			}

			if (pos == 0)
			{
				pos = 8;
				ch = fgetc(fread);
			}
		}

		fclose(fread);
		fclose(fwrite);
		cout<<"解压缩成功"<<endl;
	}
private:
	//读取配置文件
	void _ReadConfigFile(const char* configfilename, CharInfo* info)
	{
		FILE* fread = fopen(configfilename, "rb");
		if (NULL == fread)
		{
			cout<<"Read File Fault"<<endl;
			exit(0);
		}

		int ch = fgetc(fread);
		while (ch != EOF)//一直读取到文件结尾
		{
			//字符,字符出现的次数,字符编码
			info[ch]._ch = ch;
			unsigned char index = ch;//记录当前的下标,一遍后面使用

			//因为这里是一个“,”,所以要将他读取并读取下一个字符
			ch = fgetc(fread);
			ch = fgetc(fread);

			string count;
			while (ch != ',')
			{
				count.push_back(ch);
				ch = fgetc(fread);
			}
			info[index]._count = atoi(count.c_str());//将获取的字符出现的次数存入。

			ch = fgetc(fread);
			//将字符编码依次存入
			while (ch != '\n')
			{
				info[index]._code.push_back(ch);
				ch = fgetc(fread);
			}

			ch = fgetc(fread);//读取'\n'字符的下一个字符
		}
	}

	//写配置文件
	void _WriteConfig(const char* filename)
	{
		//压缩文件的信息保存在“.config”后缀的文件当中
		string write(filename);
		write += ".config";
		FILE* fwite = fopen(write.c_str(), "wb");//只写形式打开文件

		for (int i = 0;i < 256;i++)
		{
			if (_infos[i]._count)
			{
				//存放形式为(字符,字符出现的次数, 字符编码)
				fputc(_infos[i]._ch, fwite);
				fputc(',', fwite);
				
				//将字符出现的次数以十进制字符的形式存入字符数组中
				char arr[126];
				_itoa(_infos[i]._count, arr, 10);
				fputs(arr, fwite);
				fputc(',', fwite);

				fputs(_infos[i]._code.c_str(), fwite);
				fputc('\n', fwite);
			}
		}
		//关闭文件实际上就是保存文件
		fclose(fwite);
	}
	//后序遍历哈夫曼树,我们只需要访问到叶子结点
	//获取huffman编码
	void _GetHuffmanCode(Node* root, string code)
	{
		if (NULL == root)
		{
			return;
		}

		//后序遍历,左右根
		//编码左0右1
		_GetHuffmanCode(root->_left, code + '0');
		_GetHuffmanCode(root->_right, code + '1');

		if (root->_left == NULL && root->_right == NULL)
		{
			_infos[root->_weight._ch]._code = code;
		}
	}
private:
	CharInfo _infos[256];//创建一个CharInfo类型的数组,利用哈希性质,每个位置的_data值都是对应的字符
};



经过测试,该压缩程序确实可以压缩音频,图片,文本等等,能够压缩并且解压会原来的一模一样。但是压缩后只有文本的大小变小了,其他类型的文件大小却并没有变小。

压缩文本的比例大概在70%-80%之间


压缩“.pdf”文件大小并没有减小,图片同样没有减小


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值