文件压缩

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

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

项目特点:

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

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

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

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

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

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


代码如下:

哈弗曼树的建立:

#pragma once

#include"heap.hpp"
//哈弗曼树:最优二叉树,加权路径长度最短的二叉树【贪心算法】
template<class T>
struct HuffmanNode
{
	HuffmanNode* _left;
	HuffmanNode* _right;
	T _weight;
	HuffmanNode(const T& weight = 0)
		:_left(NULL)
		, _right(NULL)
		, _weight(weight)
	{}
};
template<class T>
class HuffmanTree
{
	typedef HuffmanNode<T> Node;
public:
	HuffmanTree()
		:_root(NULL)
	{}
	HuffmanTree(const T* a, size_t size, const T& invalid)//构造哈夫曼树,invalid为非法值
	{
		assert(a);
		_root = CreateTree(a, size, invalid);
	}
	Node* GetRootNode()//获得根节点
	{
		return _root;
	}
	~HuffmanTree()
	{
		Clear(_root);
	}
private:
	Node* CreateTree(const T* a, size_t size, const T& invalid)//构造哈夫曼树
	{
		assert(a);
		struct compare
		{
			bool operator()(const Node* r1, const Node* r2)
			{
				return r1->_weight < r2->_weight;
			}
		};
		Heap<Node*, compare> minHeap;//所有元素建成小堆,每次取出最小值进行
		for (size_t i = 0; i < size; ++i)
		{
			if (a[i] != invalid)
			{
				minHeap.Push(new Node(a[i]));
			}
		}
		Node* parent = new Node(0);
		while (minHeap.Size() > 1)
		{
			Node* left = minHeap.Top();
			minHeap.Pop();
			Node* right = minHeap.Top();
			minHeap.Pop();

			parent = new Node(left->_weight + right->_weight);
			parent->_left = left;
			parent->_right = right;

			minHeap.Push(parent);
		}
		return parent;
	}
	void Clear(Node* root)
	{
		if (root->_left)
			Clear(root->_left);
		if (root->_right)
			Clear(root->_right);
		delete root->_left;
		delete root->_right;
	}
private:
	Node* _root;
};
在进行压缩和解压缩文件的编写,要使其可以独立进行,必须添加一个配置文件的编写。代码如下所示:
#include"HuffmanTree.hpp"
#include<fstream>
#include<string>
#include<stdio.h>
#define _DEBUG_//文件压缩,大文件出错:文件没有读完,压缩的文件中(随意组成)存在控制字符

typedef long long LongType;
struct CharInfo
{
	unsigned char _ch; //字符
	LongType _count;  //字符出现的次数
	string _code;     //huffman code

	CharInfo(const int& count = 0)//不用初始化string
		:_ch(0)
		, _count(count)
	{}
	bool operator < (const CharInfo& info)const
	{
		return _count < info._count;
	}
	bool operator != (const CharInfo& info)const
	{
		return _count != info._count;
	}
	CharInfo operator + (const CharInfo& info)const
	{
		return  CharInfo(_count + info._count);
	}
};
class FileCompress
{
public:
	FileCompress()
	{
		for (int i = 0; i < 256; ++i)
		{
			_infos[i]._ch = i;
			_infos[i]._count = 0;
		}
	}
	void Compress(const char* filename)//压缩文件
	{
		//读取文件,统计文件字符出现的次数
		FILE* fOut = fopen(filename, "rb");//对于大文件,考虑二进制读写文件
		assert(fOut);
		char ch = fgetc(fOut);//不用unsigned
		while (!feof(fOut))//FEOF:检查文件标志位(返回-1结束)一定可以读到文件的末尾,在文件中做了文件标志位
		{
			_infos[(unsigned char)ch]._count++;//转化为unsigned
			ch = fgetc(fOut);
		}
		
		//构造Huffman Tree
		CharInfo invalid(0);//设置非法值
		HuffmanTree<CharInfo> tree(_infos, 256, invalid);
		
		//生成Huffman Code
		string cd;
		GenerateHuffmanCode(tree.GetRootNode(), *this, cd);

		//配置文件:存放存在的字符及对应的huffman编码,还有进行压缩的字符总个数
		string CompressConfig = filename;
		CompressConfig += ".Config";
		FILE* fInConfig = fopen(CompressConfig.c_str(), "wb");
		assert(fInConfig);

		//总字符数为树的根结点的权重
		string str; //使用str来保存出现的字符
		char buffer[128] = { 0 };
		for (size_t i = 0; i < 256; ++i)
		{
			if (_infos[i]._count > 0)
			{
				str += _infos[i]._ch;
				str += ',';
				//第一个参数是要转换的数字,第二个参数是要写入转换结果的目标字符串,第三个参数是转移数字时所用的基数
				_itoa(_infos[i]._count, buffer, 10);//表示10进制的int类型转换成char类型
				str += buffer;
				str += '\n';
				fputs(str.c_str(), fInConfig);
				str.clear();//每次对str进行清除,就能够保存下一个出现的字符
			}
		}

		//压缩文件
		string uncompress = filename;
		uncompress += ".uncompress";
		FILE* fIn = fopen(uncompress.c_str(), "wb");
		//读取文件,找出对应字符的Huffman编码,转化成为位
		fseek(fOut, 0, SEEK_SET);//函数fseek()给出的流设置位置数据,SEEK_SET为从文件的开始处开始搜索.fseek()成功时返回0, 失败时返回非零.
		ch = fgetc(fOut);
		int value = 0;
		int pos = 0;
		while (!feof(fOut))//!feof(fOut)(针对大文件)
		{
			string code = _infos[(unsigned char)ch]._code;
			for (size_t i = 0; i < code.size(); ++i)
			{
				value <<= 1;
				if (code[i] == '1')//Huffman编码中有1的,将对应位设为1
				{
					value |= 1;
				}
				if (++pos == 8)
				{
					fputc(value, fIn);//写入一个字符后pos和value置0
					pos = 0; value = 0;
				}
			}
			ch = fgetc(fOut);
		}
		if (pos != 0)
		{
			fputc(value << (8 - pos), fIn);//不足一个字节的后面补0
		}
		fclose(fOut);
		fclose(fIn);
		fclose(fInConfig);
	}
	void GenerateHuffmanCode(HuffmanNode<CharInfo>* root, FileCompress& file, string code)//递归实现Huffman Code
	{//左0右1
		if (root == NULL)
			return;
		if (root->_left == NULL && root->_right == NULL)//叶子结点
		{
			_infos[root->_weight._ch]._code = code;
#ifdef _DEBUG_
			cout << root->_weight._ch << ": " << code << endl;//调试信息
#endif _DEBUG_
		}
		//code.push_back('0');
		//GenerateHuffmanCode(root->_left, file, code);//string中可以+
		//code.pop_back();

		//code.push_back('1');
		//GenerateHuffmanCode(root->_right, file, code);
		//code.pop_back();

		if (root->_left)
		{
			GenerateHuffmanCode(root->_left, file, code + '0');//string中可以+
		}
		if (root->_right)
		{
			GenerateHuffmanCode(root->_right, file, code + '1');
		}
	}

	void UnCompress(const char* filename)//解压缩文件
	{
		//从配置文件中读取文件的字符及对应字符出现的次数,得到_infos[256]
		FILE* fOutConfig = fopen("Input.BIG.config", "rb");
		assert(fOutConfig);

		//修改_count,注意\n,有可能代表字符,有可能是行结束标志
		//原型 char *  fgets(char * s, int n,FILE *stream);
		//参数:
	    //s : 字符型指针,指向存储读入数据的缓冲区的地址。
	    //n : 从流中读入n - 1个字符
        //stream : 指向读取的流。
		//返回值:
		//1. 当n <= 0 时返回NULL,即空指针。
		//2. 当n = 1 时,返回空串"".
		//3. 如果读入成功,则返回缓冲区的地址。
		//4. 如果读入错误或遇到文件结尾(EOF),则返回NULL.

		//fgets(...)读入文本行的两种情况
		//1、如果n大于一行的字符串长度,那么当读到字符串末尾的换行符时,fgets(..)会返回。
		//并且在s的最后插入字符串结束标志'\0'。 而s缓冲区剩余的位置不会再填充。
		//2、如果n小于等于一行的字符串的长度,那么读入n-1个字符,此时并没有读入\n因为并没有到行尾 ,同样在最后会插入'\0'.
		
		//fgets(...)读入整个文件内容
		//通常用while()循环来使fges()读入文本全部内容,并按行读入
		//当然如果n小于每行的字符个数,也可以读,只不过读的次数要多。n++

		//fgets(...)从标准设备读数据。
		//用fgets(...)还也读入标准输入设备(一般为键盘)的信息
		//原型  :  fgets(s, n, stdin);
		//假设在控制台下,我们可以用fgets(...)替代gets(), 读入键盘输入的信息,fgets()是安全的,因为不会像gets()有溢出的可能。。

		unsigned char ch = fgetc(fOutConfig);
		char buff[128] = { 0 };
		while (!feof(fOutConfig))
		{
			fgetc(fOutConfig);//读取了逗号
			fgets(buff, 128, fOutConfig);//fgets(...)读入文本行
			_infos[ch]._count = (LongType)atoi(buff);
			ch = fgetc(fOutConfig);//读取下一行首字符
		}

		//读取压缩文件
		FILE* fOut = fopen(filename, "rb");
		assert(fOut);

		//解压后的文件
		string compare = filename;
		compare += ".compare";
		FILE* fIn = fopen(compare.c_str(), "wb");
		assert(fIn);

		//建立哈夫曼树
		CharInfo invalid(0);//设置非法值
		HuffmanTree<CharInfo> tree(_infos, 256, invalid);
		HuffmanNode<CharInfo>* root = tree.GetRootNode();
		HuffmanNode<CharInfo>* cur = root;
		ch = fgetc(fOut);
		int count = root->_weight._count;//总字符数为树的根结点的权重
		int pos = 7;
		while (count > 0)//读取总共count个字符并写入压缩后的字符
		{
			while (pos >= 0)
			{
				if (ch & (1 << pos))//
				{
					cur = cur->_right;
				}
				else
				{
					cur = cur->_left;
				}
				if (cur->_left == NULL && cur->_right == NULL)
				{
					fputc(cur->_weight._ch, fIn);
					if (--count == 0)//将剩余没有写入的字符总次数减1
						break; 
					cur = root;
				}
				pos--;
			}
			pos = 7;
			ch = fgetc(fOut);
		}
		fclose(fOut);
		fclose(fIn);
		fclose(fOutConfig);
	}
private:
	CharInfo _infos[256];
}; 
测试用例如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include"FileCompress.hpp"
#include<ctime>

void TestCompress()
{
	cout << "压缩时间:";
	clock_t start, finish;
	start = clock();

	FileCompress fc;
	//fc.Compress("lyf.com");
	fc.Compress("Input.BIG");

	finish = clock();
	cout << finish - start << "/" << CLOCKS_PER_SEC << " (s) " << endl;
}
void TestUnCompress()
{
	cout << "解压时间:";
	clock_t start, finish;
    start = clock();

	FileCompress fc;
	//fc.UnCompress("lyf.com.uncompress");
	fc.UnCompress("Input.BIG.uncompress");

	finish = clock();
    cout << finish - start << "/" << CLOCKS_PER_SEC << " (s) " << endl;
}
int main()
{
	//Test();
	//TestCompress();
	TestUnCompress();
	system("pause");
	return 0;
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值