用Huffman树实现文件压缩并解压

一、前言

        如果你学习数据结构,就一定会学到Huffman树,而Huffman编码实际上上就是zip压缩的核心部分,所以,如果已经学习了Huffman树,为何不尝试写一个压缩程序出来呢?

如果你没有学习Huffman树,那咱们就一起先学习一下Huffman树吧。

二、Huffman树压缩文件


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

建立:


        这样建立的树,保证所有数据成员都在叶子节点上,且数越小,离根节点越远,越大,离根节点越近,那么这样的特点应用于压缩中是很关键的,我们可以让出现次数少的字符编码长一些,次数多的字符编码短一些。接下来我们看看压缩的步骤吧~

1>统计要压缩的文件中字符出现的次数。

    遍历一遍文件,将字符出现的次数统计在一个结构体数组里,数组里包含字符,字符出现的次数,对该字符的编码。

2>用得到的数组构建一个Huffman树。

   因为每次要取最小值,所以这里考虑建立一个小堆。

3>得到Huffman编码

    怎么得到呢?向右为1,向左为0,就是这么简单,我画图示意一下:


    原本用一个char表示的字符,现在只占了几个位,这就是为什么能将文件压缩。

4>向压缩文件里写入Huffman编码。

    写入的时候,满8个位写进去,如果最后不足8个位,先补齐,解压的时候要注意,解压到源文件字符数的时候停止即可。源文件的总字符数可以在第一次遍历统计出现的字符个数时统计,还有一种方法就是,仔细观察Huffman树就知道,它的根节点的大小,其实就是所有叶子节点相加的和。所以,根节点的大小就是源文件里所有字符出现的总次数。

至此,压缩就结束了。

但是,怎么解压缩呢?解压缩至少也得已知这样的一颗树才行啊,所以,我们在压缩完成后要建立一个配置文件。

5>建立配置文件

    配置文件里要存储源文件字符及出现的次数。有了这样的配置文件,就可以再次构建Huffman树!


三、解压缩


1.根据配置文件得出出现的字符和对应出现次数 

2.再次构建huffman树
3.根据配置压缩文件和哈夫曼树进行走 一个一个字符依次写入
介绍:主要原理是通过huffman编码来重新表示字符,使得出现频率高的字符编码短,出现少的字符编码长。整体下来的话,所需的总的bit位是减少的。但是要注意当大部分字符出现的频率都差不多时,huffman压缩的压缩效率会很低。

四、我遇到的问题


1>编译时不通过,一大堆的错误,我找了半天!最后发现是一个很简单的问题,我的Huffman树使用的是C++模板实现的,模板不能分离编译,而我在压缩时建立Huffman树是在另一个文件中进行的,所以编译不通过。

解决方法:.h后缀改成.hpp,重新包一下头文件ok。

2>文件的打开方式。这里打开文件一定要用二进制形式,"wb","rb".因为二进制打开和文本打开其实是有区别的。文本方式打开,会对‘\n’进行特殊处理,那如果这个字符本身就是'\n'.这就会出现问题,所以使用二进制打开,特点:不进行任何处理,是什么就是什么。

3>压缩后解压缩的图片打不开,经过我反复查找,终于发现是配置文件里对‘\0’的处理问题,我在写配置文件起初是用一个string把字符和它出现的次数连接起来放进去。比如:a,3   这样带来的问题是  \0,200  写的时候是以c字符串的形式写的,所以遇见'\0'就终止了,那么在解压缩的时候就会出问题。

解决方法:先把字符放进去,再把剩下的构建成string对象放进去。

4>压缩汉字出现问题  汉字字符范围是0-255

5>二次压缩效率很低,压缩一次之后字符出现的次数相差不是很大


模板不可以分离编译:

在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找当遇到未决符号时它会寄希望于连接器。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来,所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。
.h后缀改成.hpp 重新包一下头文件


五、源码


1>Huffman树


//构建Huffman树

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

template<class W>
struct HuffmanTreeNode
{
	HuffmanTreeNode<W>* _left;
	HuffmanTreeNode<W>* _right;
	HuffmanTreeNode<W>* _parent;
	//里面存的是个W 结构的魅力
	W _w; // Ȩֵ


	//三叉树 方便构建
	HuffmanTreeNode(const W& w)
		:_left(NULL)
		,_right(NULL)
		,_parent(NULL)
		,_w(w)
	{}
};

template<class W>
class HuffmanTree
{
public:
	typedef HuffmanTreeNode<W> Node;

	HuffmanTree()
		:_root(NULL)
	{}

	HuffmanTree(W* w, size_t n, const W& invalid)
	{
		//适配器
		struct NodeComapre
		{
			bool operator()(Node* l, Node* r)
			{
				return l->_w < r->_w;
			}
		};
		Heap<Node*, NodeComapre> minHeap;
		for (size_t i = 0; i < n; ++i)
		{

			//重载了!=
			if (w[i] != invalid)
				minHeap.Push(new Node(w[i])); // 一步顶两步
		}

		while (minHeap.Size() > 1)
		{
			Node* left = minHeap.Top();
			minHeap.Pop();
			Node* right = minHeap.Top();
			minHeap.Pop();
								//重载了+
			Node* parent = new Node(left->_w + right->_w);
			parent->_left = left;
			parent->_right = right;
			left->_parent = parent;
			right->_parent = parent;
			minHeap.Push(parent);
		}

		_root = minHeap.Top();
	}
	~HuffmanTree()
	{
		Destory(_root);
		_root = NULL;
	}

	Node* GetRoot()
	{
		return _root;
	}

protected:
	void Destory(Node* root)
	{
		if (root == NULL)
			return;

		Destory(root->_left);
		Destory(root->_right);
		delete root;
	}


private:
	Node* _root;
};
2>压缩和解压缩
#pragma once  
#pragma warning(disable:4996)  
#include<stdio.h>
#include<cassert>  
#include<Windows.h>  
#include<string>  
#include<iostream>  
#include"HuffmanTree.hpp"

struct weight
{
	long long _count; //出现次数
	char _ch;		//字符
	string _code;	//编码

	weight(long long count=0)
		:_count(count)
		,_ch(0)
		,_code("")
	{}
	weight operator+(const weight& w)
	{
		long long tmp = _count + w._count;
		return weight(tmp);
	}
	bool operator<(const weight& w)
	{
		return _count < w._count;
	}
	bool operator!=(const weight& w)
	{
		return !(_count == w._count);
	}
};
class HuffmanPress
{
public:
	HuffmanPress()
	{
		for (int i = 0; i < 256; i++)
		{
			_infos[i]._ch = i;

		}
	}
	bool FilePress(const char* filename)
	{
		//1.统计各个字符出现的次数
		FILE* fout = fopen(filename, "rb");
		assert(fout);
		//ch定义成int  有可能超过127
		char ch = fgetc(fout);
		long long charcount = 0;
		while (1)
		{
			if (feof(fout))
				break;
			_infos[(unsigned char)ch]._count++;
			ch = fgetc(fout);
			charcount++;
		}
		//2.构建HuffmanTree
		weight invalid(0);
		HuffmanTree<weight> hp(_infos, 256, invalid);
		//GetRoot()返回的是Node*  
		//3.获取Huffman编码
		HuffmanTreeNode<weight>* root = hp.GetRoot();
		string code;
		_GetCodeR(root, code);
		//4.给压缩文件中写哈夫曼编码
		string Compreesfile = filename;
		Compreesfile += ".huffman";
		FILE* fin = fopen(Compreesfile.c_str(), "wb");
		assert(fin);
		//统计完次数 最后需要是指针指向文件开头
		fseek(fout, 0, SEEK_SET);
		ch = fgetc(fout);
		int value = 0;
		int pos = 0;
		while (!feof(fout))
		{
			string s = _infos[(unsigned char)ch]._code;
			for (size_t i = 0; i < s.size(); i++)
			{
				value <<= 1;
				if (s[i]=='1')
				{
					value |= 1;
				}
				if (++pos==8)
				{
					fputc(value, fin);
					value = 0;
					pos = 0;
				}
			}
			ch = fgetc(fout);
		}
		if (pos)//最后编码不足一字节
		{
			value = value << (8 - pos);
			fputc(value, fin);
		}
		//配置文件
		string Configfile = filename;
		Configfile += ".config";
		FILE* fcon = fopen(Configfile.c_str(), "wb");
		assert(fcon);
		char countstr[20];//字符出现的次数
		//把这些全部用字符保存
		//(source,dstin,进制) 保留了高32位
		itoa(charcount >> 32, countstr, 10);
		fputs(countstr, fcon);
		fputc('\n', fcon);
		//与运算 和1相&是本身  保留了低32位 总结一下相关位运算
		itoa(charcount & 0xffffffff, countstr,10);
		fputs(countstr, fcon);
		fputc('\n', fcon);
		
		for (int i = 0; i < 256; i++)
		{
			string s;
			if (_infos[i]!=invalid)
			{
				fputc(_infos[i]._ch, fcon);
				fputc(',', fcon);
				itoa(_infos[i]._count, countstr, 10);
				s += countstr;
				fputs(s.c_str(), fcon);
				fputc('\n', fcon);
			}
		}
		//这种方法会重复计算 比如有两个a 会记录两次
		//while (ch!=EOF)
		//{
		//	string s;
		//	if (_infos[ch]!=invalid)
		//	{
		//		fputc(ch, fcon);
		//		fputc(',', fcon);
		//		itoa(_infos[ch]._count, countstr, 10);
		//		s += countstr;
		//		fputs(s.c_str(), fcon);
		//		fputc('\n', fcon);//一行结束
		//	}
		//	ch = fgetc(fout);

		//}
		fclose(fout);
		fclose(fin);
		fclose(fcon);
		return true;

	}
	bool FileUncompress(char* filename)  //这里给的是压缩文件名
	{
		//文件操作
		string  Configfile = filename;
		int tmp = Configfile.rfind('.');
		Configfile = Configfile.substr(0, tmp);
		string Uncompress = Configfile + ".uncompress";
		FILE* fucon = fopen(Uncompress.c_str(), "wb");//反压缩文件
		assert(fucon);
		//这个是读
		FILE* fin = fopen(filename, "rb");//哈夫曼文件(哈夫曼编码保留的文件)
		assert(fin);
		Configfile += ".config";
		FILE* fcon = fopen(Configfile.c_str(), "rb");//配置文件  出现的字符及其对应的出现次数(重建哈夫曼树) 和总字符数
		assert(fcon);



		string s;
		_ReadLine(fcon, s);
		long long count = atoi(s.c_str());//总字数
		s.clear();
		_ReadLine(fcon, s);
		count <<= 32;
		count += atoi(s.c_str());
		s.clear();
		while (_ReadLine(fcon,s))
		{
			if (!s.empty())
			{

				char ch = s[0];
				_infos[(unsigned char)ch]._count = atoi(s.substr(2).c_str());
				s.clear();
			}
			//这里不懂
			else
			{
				s += '\n';
			}

		}
		//再次构建Huffman树
		weight invalid(0);
		HuffmanTree<weight> hp(_infos, 256, invalid);
		HuffmanTreeNode<weight>* root = hp.GetRoot();
		HuffmanTreeNode<weight>* cur = root;
		char ch = fgetc(fin);
		int pos = 8;//pos是8
		while (1)
		{
			--pos;
			if ((ch>>pos)&1)
			{
				cur = cur->_right;
			}
			else
			{
				cur = cur->_left;
			}
			if (cur->_left==NULL&&cur->_right==NULL)
			{
				fprintf(fucon,"%c", cur->_w._ch);
				//fputc(cur->_w._ch, fucon);
				cur = root;
				count--;
			}
			if (pos==0)
			{
				ch = fgetc(fin);
				pos = 8;
			}
			if (count==0)
			{
				break;
			}
			
		}
		fclose(fin);
		fclose(fcon);
		fclose(fucon);
			
		return true;
	}

protected:
	bool _ReadLine(FILE* filename, string& line)
	{
		assert(filename);
		if (feof(filename))
			return false;
		///时刻注意这里是unsigned char
		unsigned char ch = fgetc(filename);
		
		while (ch != '\n')
		{
			line += ch;
			ch = fgetc(filename);
			if (feof(filename))
				return false;
		}
		return true;
	}
	void _GetCodeR(HuffmanTreeNode<weight>* root,string code)
	{
		if (root==NULL)
			return;
		if (root->_left==NULL&&root->_right==NULL)
		{
			_infos[(unsigned char)root->_w._ch]._code = code;
		}
		_GetCodeR(root->_left,code + '0');
		_GetCodeR(root->_right,code + '1');
	}
private:
	weight _infos[256];
};





void Test()
{
	HuffmanPress h;
	h.FilePress("test.txt");
	h.FileUncompress("test.txt.huffman");
}

int main()
{
	Test();
	system("pause");
	return 0;
}


Heap.h

#include<iostream>
using namespace std;
#include<vector>

template<class T>
struct Greater
{
	bool operator()(const T& x, const T& y)
	{
		return x > y;
	}
};
template<class T>
struct Less
{
	bool operator()(const T&x, const T& y)
	{
		return x < y;
	}
};
template<class T, class Compare = Grater<T>>
class Heap
{
public:
	Heap()
	{}

	Heap(const T* a, size_t n)
	{
		//vector(动态数组) 
		//vector reserve() 改变Capacity 配套用push_back 建议使用这个
		//vector resize()  改变size大小  a[i] = array[i]
		_v.reserve(n);
		for (size_t i = 0; i < n; i++)
		{
			_v.push_back(a[i]);
		}
		//建堆
		for (int i = ((int)_v.size() - 2) / 2; i >= 0; i--)
		{
			AdjustDown(i);
		}
		//向下调正算法给的第一个就是父节点 向上给的第一个是叶子节点
		//向下调正算法 假设左右都是大堆 往下换 叶子节点不用算,从最后一个非叶子节点算起(倒着算)
	}

	void Push(const T& data)
	{
		_v.push_back(data);
		//向上调正算法 只在push()这里用,把新填的叶子节不断向上换 直到符合大/小堆
		AdjustUp(_v.size() - 1);
	}
	void Pop()
	{
		//1.先将堆顶元素与堆的最后一个元素交换
		swap(_v[_v.size() - 1], _v[0]);
		//2.删除最后一个节点
		_v.pop_back();
		//3.向下调整
		AdjustDown(0);
	}
	T& Top()
	{
		return _v[0];
	}
	size_t Size()
	{
		return _v.size();
	}
protected:
	void AdjustDown(int root)
	{
		int parent = root;
		int child = root * 2 + 1;
		while (child<(int)_v.size())
		{
			if (child + 1 < (int)_v.size() && Compare()(_v[child + 1], _v[child]))
			{
				child++;
			}
			if (Compare()(_v[child], _v[parent]))
			{
				swap(_v[parent], _v[child]);
				parent = child;
				child = parent * 2 + 1;
			}
			else
			{
				break;
			}
		}
	}
	void AdjustUp(int child)
	{
		int parent = (child - 1) / 2;
		//注意这里是child>0 不是parent>0
		while (child>0)
		{
			if (Compare()(_v[parent], _v[child]))
			{
				break;
			}
			else
			{
				swap(_v[child], _v[parent]);
				child = parent;
				parent = (child - 1) / 2;
			}

		}
	}

private:
	vector<T> _v;
};


猜你喜欢

转载自blog.csdn.net/qq_25424545/article/details/78282978