huffman树-------文件压缩

哈夫曼树(霍夫曼树)又称为最优树.
1、路径和路径长度
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。
2、结点的权及带权路径长度
若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
3、树的带权路径长度
树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。


有了上面的概念,那么下面再给出huffman树的概念:

           假设有n个权值{w1,w2,w3........,wn},试构造一棵有n个叶子结点的二叉树,每个叶子结点带权为wi,则其中带权路径长度WPL最小的二叉树称作最优二叉树或赫夫曼树。

那么下面是构造一棵赫夫曼树的方法:

假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。

下面是huffman.h的实现代码:

#pragma once
#include<queue>
template<typename T>struct Nod{
	T _w;
	struct Nod *_left;
	struct Nod *_right;
	Nod(T const& w) :_w(w), _left(NULL), _right(NULL){}
};
template<typename T> class Huffman{
public:
	typedef Nod<T> Node;
	template<typename Nod>struct Greater{        //仿函数,比较结点里面的值域的大小
		bool operator()(Nod const& l, Nod const& r)
		{
			return l->_w > r->_w;
		}
	};
	Huffman() = default;//默认构造函数
	Huffman(T* w,size_t N,T invalue)//传一个数组名和数组的长度进来
	{
		//priority_queue<Node> pq0;//优先级队列默认大根堆   less的意思是孩子小于双亲
		priority_queue<Node*, vector<Node*>, Greater<Node*> > pq;//小根堆,greater的意思是孩子大于双亲
		for (size_t i = 0; i < N; ++i){
			if (w[i] != invalue)
				pq.push(new Node(w[i]));//建立小根堆,压进去是Node*
		}
		_root = CreatTree(pq);
	}
	Node* GetRoot()
	{
		return _root;
	}
private:
	Node *_root;
	Huffman(Huffman<T> const&);//防拷贝
	Huffman<T>& operator=(Huffman<T> const&);//防复制
	Node* CreatTree(priority_queue<Node*, vector<Node*>, Greater<Node*>> &pq)
	{
		while (pq.size() > 1){
			Node *left = pq.top(); pq.pop();
			Node *right = pq.top(); pq.pop();
			Node *parent = new Node(left->_w + right->_w); pq.push(parent);

			parent->_left = left;parent->_right = right;
		}
		return pq.top();
	}
};


---------------------------------------------我是分界线-------------------------------------------------------------------------


     构建完了huffman树,开始进行文件压缩。文件压缩可以分为五个步骤:

  • 1. 统计字符出现的次数
  • 2. 构建HuffmanTree
  • 3. 生成哈夫曼编码 (Huffman Code)
  • 4. 压缩 (compress)
  • 5. 解压缩 (uncompress)


 下面是文件具体的压缩过程:

Input.txt 
内容:aaaabbbcccd

1. 统计字母出现的次数:

a —— 4 
b —— 3 
c —— 2 
d —— 1

2. 构建huffmanTree:

这里写图片描述

3. 生成huffman code(编码):

a -> 0 (* 出现次数多的字符(数值大)–路径短–编码短) 
b -> 11 
c -> 101 
d -> 100

4. 压缩(按位存储):

文件 :Input.txt.huffman 
内容 : 00001111 11101101 100(00000) —-按位存储 
第三个字节后面的五个0是补位 
(原文件占用10个字节,压缩后占用3个字节)

5. huffman解压缩:

还原文件:Input.txt.unhuffman

文件压缩的完整实现代码:

包括huffman.h,compressfile.h和test.cpp。

huffman.h:(如上代码)

compressfile.h

#pragma once
#include"huffman.h"
#include<string>
class FileCompress{
public:
	struct _charinfos{//内置类型
		char _ch;
		size_t _count;
		std::string _code;
		bool operator!=(_charinfos const& charin)//如果结构体中_count不相等,那么结构体不相等
		{
			return _count != charin._count;
		}
		bool operator>(_charinfos const& charin)
		{
			return _count > charin._count;
		}
		_charinfos operator+(_charinfos const& charin)
		{
			_charinfos tmp;
			tmp._count = _count + charin._count;
			return tmp;
		}
	};
	typedef Nod<_charinfos> Node;
	struct _tmpinfos{
		char _ch;
		size_t _count;
	};
	FileCompress()//默认构造函数
	{
		for (size_t i = 0; i < 256; ++i){
			charinf[i]._ch = i;
			charinf[i]._count = 0;
		}
	}
	void Compress(const char* file)//huffman.txt
	{
		//1.打开一个文件,对其字符进行统计出现的频率
		FILE *fsrc = fopen(file, "rb");
		char ch = fgetc(fsrc);
		while (!feof(fsrc)){
			++charinf[(unsigned char)ch]._count;
			ch = fgetc(fsrc);
		}
		//2.构建一棵赫夫曼树并得到huaffman code
		_charinfos invalue; invalue._count = 0;
		Huffman<_charinfos> h(charinf, 256, invalue);//数组向指针的转化
		auto root = h.GetRoot(),cur = root; string str;
		Traverse(cur, str);//遍历赫夫曼树,得到huffman code
		//3.压缩文件,把1.中统计的字符放到压缩文件中去
		string filedst(file);
		filedst += ".huffman";
		FILE *fdst = fopen(filedst.c_str(), "wb");//打开目标文件
		_tmpinfos tmpinf;
		for (size_t i = 0; i < 256; ++i){//为了尽可能方便且少的将信息压入目标文件
			if (charinf[i]._count){
				tmpinf._ch = i;tmpinf._count = charinf[i]._count;
				fwrite(&tmpinf, sizeof(_tmpinfos), 1, fdst);//将有用的信息写入目标文件
			}
		}tmpinf._count = 0;
		fwrite(&tmpinf, sizeof(_tmpinfos), 1, fdst);//多写入一个信息,用作信息结束标志

		rewind(fsrc);//将stream指向的流的文件定位符设置在文件的开始位置,等价于fseek(fsrc,0,SEEK_SET)
		char ch1 = fgetc(fsrc), ch2 = 0; size_t pos = 0;
		while (!feof(fsrc)){
				string code = charinf[(unsigned char)ch1]._code;
				for (size_t j = 0; j < code.size(); ++j){
					ch2 |= code[j] - '0' << pos++;
					if (pos == 8){
						//int test55 = ch2; cout << test55 << " ";//验证信息,查看到底压入了什么数据
						fputc(ch2, fdst);//向目标文件中写入一个每个位都被重置了的字符
						ch2 = 0; pos = 0;
					}
				}
				ch1 = fgetc(fsrc);
		}
		//还要考虑当原文件读到最后一字符个时,ch2的8个位没有被全部重置的情况
		if (pos){ /*int test55 = ch2; cout << test55 << " ";*/
			fputc(ch2, fdst);
		}
		fclose(fsrc);
		fclose(fdst);
	}
	void Uncompress(const char* file)//huffmanhzq.txt
	{
		//4.根据压缩文件重构赫夫曼树
		FILE *fout = fopen(file, "rb");
		_tmpinfos tmpinf;
		fread(&tmpinf, sizeof(_tmpinfos), 1, fout);
		while (tmpinf._count){//根据压缩文件,对其字符进行统计出现的频率
			charinf[(unsigned char)tmpinf._ch]._ch = tmpinf._ch;
			charinf[(unsigned char)tmpinf._ch]._count = tmpinf._count;
			fread(&tmpinf, sizeof(_tmpinfos), 1, fout);
		}
		_charinfos invalue; invalue._count = 0;
		Huffman<_charinfos> h(charinf, 256, invalue);//重构赫夫曼树
		//5.解压缩文件(root->_w._count记录了原文件中所有字符出现的次数)
		string filedst(file);
		filedst.erase(filedst.rfind('.')); filedst += ".unhuffman";
		FILE *fin = fopen(filedst.c_str(), "wb");//以写的方式打开文件
		auto root = h.GetRoot(), cur = root;
		char ch = fgetc(fout); size_t n = root->_w._count;
		while (!feof(fout)){
			//int test55 = ch; cout << test55 << " ";//验证信息,查看到底拿到了什么数据
			for (size_t i = 0; i < 8; ++i){
				if (ch & 1 << i){ cur = cur->_right; }
				else{ cur = cur->_left; }

				if (!cur->_left && !cur->_right){
					fputc(cur->_w._ch, fin);
					--n;//因为n记录了原文件所有字符出现的次数,所以当n==0时表示已经翻译完
					cur = root;
				}
				if (!n)break;
			}
			ch = fgetc(fout);
		}
		fclose(fout);
		fclose(fin);
	}
private:
	_charinfos charinf[256];
	void Traverse(Node* const& root, string str)//遍历赫夫曼树,得到huffman code
	{
		if (!root->_left && !root->_right){//走到叶子结点将huffman code信息写入并返回
			charinf[(unsigned char)root->_w._ch]._code = str;
			return;
		}
		Traverse(root->_left, str + '0');
		Traverse(root->_right, str + '1');
	}
};
test.cpp:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include<CoreWindow.h>
#include"FileCompress.h"

void test_compressfile()
{
	FileCompress fc;
	fc.Compress("input.txt");      //从input.txt到input.txt.huffman
	fc.Uncompress("input.txt.huffman"); //从input.txt.huffman到input.txt.unhuffman

	//FileCompress fc;
	//fc.Compress("hehe.jpg");      //从hehe.jpg到hehe.jpg.huffman
	//fc.Uncompress("hehe.jpg.huffman");//从hehe.jpg.huffman到hehe.jpg.unhuffman
}
int main()
{
	test_compressfile();
	system("pause");
	return 0;
}


最后讲一下在进行文件压缩的时候可能会遇到的问题:(ps:别人总结的很好,下面是摘抄笔记)

1.压缩带中文的文件,程序就会崩溃。

最后发现数组越界的问题. \ 
因为char它的范围是-128~127,程序中使用char类型为数组下标(0~127),所以字符没有问题,但是汉字是占两个字节的,所以会出现越界的问题,解决的方法就是char类型强转为unsigned char,它的范围为0~255.

2.解压缩文件生成后会丢失很多内容.

1. 文件的打开方式.
   这里打开文件一定要用二进制形式,"wb","rb".因为二进制打开和文本打开其实是有区别的。
   1.文本方式打开:会对‘\n’进行特殊处理,那如果这个字符本身就是'\n'.这就会出现问题。
   2.二进制方式打开:不进行任何处理,是什么就是什么。
2. 文件结束符的问题。
   刚开始用的文件结束标志是EOF在ASII中它的编码是-1,随后我们用了 二进制方式打开文件,而二进制文件是会出现-1的,所以提前结束了,所以我们用二进制读取并且不能用EOF来判断结束。
   C中有一个函数叫做feof(),它可以检查流上文件的结束符,如果文件结束,则返回非0值,否则返回0

下面引用百科的解释: 
feof(fp)有两个返回值:如果遇到文件结束,函数feof(fp)的值为非零值,否则为0EOF是文本文件结束的标志。在文本文件中,数据是以字符的ASCⅡ代码值的形式存放,普通字符的ASCⅡ代码的范围是32127(十进制),EOF16进制代码为0x1A(十进制为26),因此可以用EOF作为文件结束标志。[1]

当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为解决这一个问题,ASCI C提供一个feof函数,用来判断文件是否结束。feof函数既可用以判断二进制文件又可用以判断文本文件。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

猜你喜欢

转载自blog.csdn.net/qq_39290388/article/details/79958677
今日推荐