文件压缩

5 篇文章 0 订阅
2 篇文章 0 订阅

Huffman树的应用-------实现文件压缩项目



Huffman树的相关定义:
WPL(带权路径长度) = PL*weight
PL (最小路径长度)= 完全二叉树的路径长度
路径(path):从树中一个结点到另一个结点之间的分支构成该俩点之间的路径。
路径长度:指路径上的分支条数。树的路径长度是从树的根结点到每一个结点的路径长度之和。
带权路径长度最小的二叉树是权值大的外结点(带有权值的叶子结点)离根结点最近的扩充二叉树,这就是Huffman树。
Huffman树又称为最优二叉树,是一类加权路径长度最短的二叉树,在编码设计、决策和算法设计等领域有着广泛应用。

用贪心算法去构建Huffman树, 贪心算法
指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的选择,而是某种意义上的局部最优解,贪心算法不是对所有问题得到整体最优解。
注:在贪心算法中使用了一个最小堆,利用它从中选择根结点权值最小和次小的俩颗树。
如下图所示:



构建Huffman树的算法会用到最小堆,则实现堆的代码如下:

Heap.h

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

//仿函数

template <class T>
struct GreaterCompare
{
	bool operator()(const T& l, const T& r)
	{
		return l > r;
	}
};

template<class T>
struct LessCompare
{
	bool operator()(const T& l, const T& r)
	{
		return l < r;
	}
};

template <class T,class Compare = LessCompare<T>>
class Heap
{
public:
	Heap()
	{}

	Heap(const T* arr, int len)
	{
		assert(arr);
		assert(len > 0);
		int i = 0;
		//传入数据
		for (; i < len; ++i)
		{
			_arr.push_back(arr[i]);
		}
		//建堆
		i = _arr.size();
		for (i = (i - 2) / 2; i >= 0;--i)
		{
			_AdjustDown(i);
		}
	}

	int Size()
	{
		return _arr.size();
	}

	bool Empty()
	{
		return _arr.empty();
	}

	//插入数据
	void Push(const T& x)
	{
		_arr.push_back(x);
		_AdjustUp(_arr.size()-1);
	}

	//删除数据
	void Pop()
	{
		if (!Empty())
		{
			swap(_arr[0], _arr[_arr.size() - 1]);
			_arr.pop_back();
			_AdjustDown(0);
		}
	}

	T Top()
	{
		return _arr[0];
	}

	~Heap()
	{}
protected:
	//向下调整
	void _AdjustDown(int root)
	{
		Compare com;
		int child = root * 2 + 1;
		while (child < (int)_arr.size())
		{
			if (child + 1 < (int)_arr.size() && com(_arr[child + 1], _arr[child]))
				++child;
			if (com(_arr[child] , _arr[root]))
			{
				swap(_arr[child], _arr[root]);
				root = child;
				child = 2 * root + 1;
			}
			else
			{
				break;
			}
		}
	}

	//向上调整
	void _AdjustUp(int child)
	{
		Compare com;
		int root = (child - 1) / 2;
		while (child > 0)
		{
			if (com(_arr[child], _arr[root]))
			{
				swap(_arr[child], _arr[root]);
				child = root;
				root = (child - 1) / 2;
			}
			else
			{
				break;
			}
		}
	}
private:
	vector<T> _arr;
};

用贪心算法构建Huffman树的代码如下:

HuffmanTree.h

#pragma once
#include"Heap.h"

template<class T>
struct HuffmanTreeNode
{
	HuffmanTreeNode<T>* _left;		//左孩子
	HuffmanTreeNode<T>* _right;		//右孩子
	T _weight;						//数据(此处称为权值)

	HuffmanTreeNode(T w = 0)
		:_left(NULL)
		, _right(NULL)
		, _weight(w)
	{}
};

//重写仿函数
template<class T>
struct HuffmanLessCompare
{
	bool operator()(HuffmanTreeNode<T>* l, HuffmanTreeNode<T>* r)
	{
		return l->_weight < r->_weight;
	}
};

template <class T, class Compare = HuffmanLessCompare<T>>
class HuffmanTree
{
	typedef HuffmanTreeNode<T> Node;
public:
	HuffmanTree()
		:_root(NULL)
	{}

	//贪心算法实现哈弗曼树的建立
	HuffmanTree(const T* arr, int len,const T invalid)
	{
		assert(arr);
		assert(len > 0);

		_root = _CreateHuffmanTree(arr, len, invalid);
	}

	Node* GetRoot()
	{
		return _root;
	}

protected:
	//压缩文件中加入invalid,当count = 0时不插入,所以文件压缩.h中引入了函数去重载!=

	Node* _CreateHuffmanTree(const T* arr, int len,const T invalid)
	{
		Heap<Node*,Compare> hp;

		for (int i = 0; i < len; ++i)
		{	
			if (arr[i] != invalid)
			{
				hp.Push(new Node(arr[i]));
			}
		}

		while (hp.Size () > 1)
		{
			Node* left =hp.Top();
			hp.Pop();
			Node* right =hp.Top();
			hp.Pop();
			
			//权值相加就是CharInfo相加,即是CharInfo中的次数相加,所以文件压缩.h中引入了函数去重载+
			Node* root = new Node(left->_weight + right->_weight);
			root->_left = left;
			root->_right = right;
			hp.Push(root);
		}
		return hp.Top();
	}

private:
	Node* _root;
};

Huffman树的应用:Huffman编码技术
Huffman编码技术是数据压缩的重要方法

Huffman编码如下图所示:



项目文件压缩的内容:
文件压缩:
1.在文本文件中,数据是以字符的ASCII码的形式存放,ASCII码的范围是0-255,所以文件压缩中用元素个数为256的数组作为底层数据结构,其中元素类型为CharInfo,包括字符,字符出现的次数,字符编码;
2.在编码前我们先要统计各个字符出现的次数;
3.根据统计的次数作为权值构建哈弗曼树;
4.递归遍历哈弗曼树生成每个字符对应的编码,编码从根结点到叶子结点;
5.将压缩编码写入压缩文件中;
6.编写配置文件保存各个字符出现的次数以便解压时重建哈夫曼树;
文件解压缩:
7.根据源文件找到配置文件;
8.将配置文件中的数据(各个字符及各个字符出现的次数)读到底层数据结构数组中;
9.根据读到的数据(数组)作为权值重新构建哈夫曼树;
10.读取压缩文件,遍历哈弗曼树,二者结合去还原源文件。

项目文件压缩的具体实现代码如下:
注:其中用到的构建Huffman树,及构建Huffman树算法中用到的最小堆的具体实现都在上面;

FileCompress.h

#pragma once

#include"HuffmanTree.h"
#include<string>


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

	CharInfo(LongType count = 0)
		:_ch(0)
		,_count(count)
	{}

	bool operator!=(const CharInfo& c) const
	{
		return _count != c._count;
	}

	CharInfo operator+(const CharInfo& c)
	{
		CharInfo tmp;
		tmp._count = _count + c._count;
		return tmp;
	}

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

class FileCompress
{
public:
	//所有字符就位
	FileCompress()
	{
		for (int i = 0; i < 256; ++i)
		{
			_arr[i]._ch = i;
		}
	}

	//文件压缩
	void CompressFile(const char* fileName)
	{
		assert(fileName);
		//1.统计文件中各个字符出现的次数
		FILE* fout = fopen(fileName, "rb");//以只读的方式打开二进制文件
		assert(fout);
		unsigned char ch = fgetc(fout);

		1.(1).统计有多少个字符,以便在解压缩时补0位的字符也多余出来
		//LongType allCount = 0;

		while (!feof(fout))	//(char)ch != EOF
		{
			//++allCount;
			_arr[ch]._count++;
			ch = fgetc(fout);
		}

		//2.(1).根据统计的次数作为权值构建哈弗曼树
		CharInfo invalid(0);
		HuffmanTree<CharInfo> hf(_arr, 256, invalid);
		
		//2.(2).生成每个字符所对应的编码
		string code;
		_HuffmanCodeOfWeight(hf.GetRoot(),code);

		//3.将压缩编码写入压缩文件中
		string CompressFileName = fileName;
		CompressFileName += ".compress";
		FILE* input = fopen(CompressFileName.c_str(), "wb");
		assert(input);

		//3.(1).重定位流(数据流/文件)上的文件内部位置指针
		fseek(fout, 0, SEEK_SET);
		int eightBit = 0;
		unsigned char bit = 0;
		ch = fgetc(fout);
		while (!feof(fout))		//(char)ch != EOF
		{
			string& code = _arr[(unsigned char)ch]._code;
			for (int i = 0; i < (int)code.size(); ++i)
			{
				bit <<= 1;

				if (code[i] == '1')
					bit |= 1;

				if (++eightBit == 8)
				{
					fputc(bit, input);
					bit = 0;
					eightBit = 0;
				}
			}
			ch = fgetc(fout);
		}
		//3.(2).将最后一个字节不够8位的往高位移动,其余补0,并写入文件
		if (eightBit)
		{
			bit <<= (8 - eightBit);
			fputc(bit, input);
		}

		//4.编写配置文件保存字符出现的次数,以便解压时重新构建哈弗曼树
		string configName = fileName;
		configName += ".config";
		FILE* configFile = fopen(configName.c_str(), "wb");//以写的方式打开二进制文件
		assert(configFile);

		//4.(1).配置文件中保存的字符的次数以字符串的形式存储
		char character[20];
		4.(2).写入总次数,以便解压缩最后一个补0的字节正常恢复
		//_itoa(allCount, character, 10);
		//fputs(character, configFile);
		//fputc('\n', configFile);
		//4.(3).写入各个字符及其出现的总次数,出现字符的格式:字符,次数\n
		CharInfo _invalid(0);
		for (int i = 0; i < 256; ++i)
		{
			if (_arr[i] != _invalid)
			{
				fputc(_arr[i]._ch, configFile);
				fputc(',', configFile);

				//_count是long long型,当大文件(字符次数出现次数超过整型)出现,
				//需要分俩段写,先写高位,再写低位?

				_itoa(_arr[i]._count, character, 10);
				fputs(character, configFile);
				fputc('\n', configFile);
			}
		}
		fclose(fout);
		fclose(input);
		fclose(configFile);
	}

	//文件的解压缩
	void UnCompress(char* fileName)
	{
		assert(fileName);
		//1.根据原文件找到配置文件
		string ConfigFileName = fileName;
		ConfigFileName += ".config";
		FILE* ConfigFileOutput = fopen(ConfigFileName.c_str(), "r");

		string line;

		2.(1).得出所有字符出现总次数
		//ReadLine(ConfigFileOutput, line);
		//LongType allCount = atoi(line.c_str());
		//line.clear();
		//2.(2).将配置文件中剩余的字符与各个字符的次数还原到数据结构CharInfo数组中
		while (ReadLine(ConfigFileOutput, line))
		{
			if (!line.empty())
			{
				unsigned char ch = line[0];
				_arr[ch]._count = atoi(line.substr(2).c_str());
				line.clear();
			}
			else
			{
				//若统计到有个空行则为换行符
				line += '\n';
			}
		}

		//3.根据配置信息读到的次数作为权值重新构建哈弗曼树
		CharInfo invalid(0);
		HuffmanTree<CharInfo> hf(_arr, 256, invalid);
		HuffmanTreeNode<CharInfo>* root = hf.GetRoot();
		HuffmanTreeNode<CharInfo>* cur = root;
		
		//4.读取压缩文件与哈弗曼树一起去还原原文件
		string CompressFileName = fileName;
		CompressFileName += ".compress";
		FILE* Output = fopen(CompressFileName.c_str(), "rb");
		assert(Output);

		string UnCompressFileName = fileName;
		UnCompressFileName += ".uncompress";
		FILE* Input = fopen(UnCompressFileName.c_str(), "wb");
		assert(Input);

		//4.(0).Huffman树的根结点的权值就是就是所有字符出现次数的总和
		LongType allCount = hf.GetRoot()->_weight._count;

		int eightBit = 8;
		unsigned char ch = fgetc(Output);
		while (!feof(Output))		//(char)ch != EOF
		{
			//4.(1).遍历哈弗曼树,通过ch去判断走左树还是走右树
			--eightBit;
			if (ch & (1 << eightBit))
				cur = cur->_right;
			else
				cur = cur->_left;

			//4.(2).读到一个字符,写入解压缩文件中,
			if (cur->_left == NULL && cur->_right == NULL)
			{
				fputc(cur->_weight._ch, Input);
				cur = root;

				//4.(3).用总次数去控制循环,防止补0位被解析为字符
				if (--allCount == 0)
				{
					break;
				}
			}

			//4.(4).8个位解析完,读取下一个字符
			if (eightBit == 0)
			{
				ch = fgetc(Output);
				eightBit = 8;
			}
		}

		fclose(ConfigFileOutput);
		fclose(Output);
		fclose(Input);
	}

	~FileCompress()
	{}

protected:
	//2.(2).生成每个字符(叶子节点)所对应的编码
	void _HuffmanCodeOfWeight(HuffmanTreeNode<CharInfo>* root, string& code)
	{
		if (root == NULL)
			return;

		//叶子结点为编码结点
		if (root->_left == NULL && root->_right == NULL)
			_arr[root->_weight._ch]._code = code;
		
		_HuffmanCodeOfWeight(root->_left, code + '0');
		_HuffmanCodeOfWeight(root->_right, code + '1');
	}

	bool ReadLine(FILE* configFile, string& line)
	{
		assert(configFile);

		unsigned char ch = fgetc(configFile);
		if ((char)ch == EOF)
			return false;

		while ((char)ch != EOF && ch != '\n')
		{
			line += ch;
			ch = fgetc(configFile);
		}
		return true;
	}

private:
	CharInfo _arr[256];
};

项目的测试用例如下:

#include<iostream>
using namespace std;
#include<assert.h>
#include"FileCompress.h"
#include<windows.h>

void TestFileCompress()
{
	FileCompress fc;
	int begin = GetTickCount();
	//fc.CompressFile("Input.txt");
	fc.CompressFile("Input.BIG");
	int end = GetTickCount();
	cout <<"FileCompressCostTime: " <<end - begin << endl;
}

void TestFileUnCompress()
{
	FileCompress fc;
	int begin = GetTickCount();
	//fc.UnCompress("Input.txt");
	fc.UnCompress("Input.BIG");
	int end = GetTickCount();
	cout <<"FileUnCompressCostTime: "<< end - begin << endl;
}

int main()
{
	TestFileCompress();
	TestFileUnCompress();
	return 0; 
}

项目中遇到的问题:

0.注意一些函数调用中的参数为const char*,而自己定义了string类型变量,记得c++到c的转换函数,string.c_str();
1.注意文本的字符范围(ASCII码)是0-255,类型为unsigned char,否则文件压缩会出现问题;
2.文件解压缩过程中的补位0的问题,用字符出现的总次数去控制循环,防止补位0被解析为字符;
3.字符出现的总次数不需要存放在配置文件中,Huffman树的根结点的权值就是所有字符出现次数的总和
4.最后一个问题让我调试了多次,就是EOF(EOF的16进制代码为0xFF(十进制为-1))是文本文件结束的标志,当把数据以二进制的形式存放到文件中时,就会有-1的值出现,因此不能采用EOF作为二进制文件的结束标志,为解决此问题,ASCII提供了一个feof函数,用来判断文件是否结束,feof既可以判断二进制文件,又可以判断文本文件

注:函数feof的简介:

函数原型:int feof(FILE* stream);
功能:检测流上的文件结束符;
返回值:如果文件结束,则返回值非0值,否则返回0;

项目测试结果如下图所示:



评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值