哈夫曼编码及文本文件的压缩解压(c++SourceCode)

 

	哈夫曼编码是一种编码方式,是可变字长编码(VLC)的一种。以哈夫曼树—即最优二叉树,带权路径长度最小的二叉树,经常应用于数据
压缩。 在计算机信息处理中,“哈夫曼编码”是一种一致性编码法(又称"熵编码法"),用于数据的无损耗压缩。这一术语是指使用一张特殊的
编码表将源字符(例如某文件中的一个符号)进行编码。这张编码表的特殊之处在于,它是根据每一个源字符出现的估算概率而建立起来的(出
现概率高的字符使用较短的编码,反之出现概率低的则使用较长的编码,这便使编码之后的字符串的平均期望长度降低,从而达到无损压缩数
据的目的)。这种方法是由David.A.Huffman发展起来的。 例如,在英文中,e的出现概率很高,而z的出现概率则最低。当利用哈夫曼编码
对一篇英文进行压缩时,e极有可能用一个位(bit)来表示,而z则可能花去25个位(不是26)。用普通的表示方法时,每个英文字母均占用一
个字节(byte),即8个位。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。倘若我们能实现对于英文中各个字母出现概率的较
准确的估算,就可以大幅度提高无损压缩的比例。
	其中哈夫曼编码的构造过程为:
	(1)初始化,根据符号概率的大小按由大到小顺序对符号进行排序。
	(2)把概率最小的两个符号组成一个新符号(节点),即新符号的概率等于这两个符号概率之和。
	(3)重复第2步,直到形成一个符号为止(树),其概率最后等于1。
	(4)从编码树的根开始回溯到原始的符号,并将每一左分枝赋值为0,右分枝赋值为0。
	前三步为构造哈夫曼树过程,既而在第四步遍历构造出的哈夫曼树,得到各字符的最优前缀码.	
	为了快速实现取集合中的最小值及向集合中插入新的元素,可以使用二叉堆来实现,二叉堆的定义及实现请另行参考.(注:由于ASCII码及
char型数据范围的限制,编码过程中生成的哈夫曼树不会非常大,所以生成哈夫曼树所需要的时间在整个压缩过程中显得微不足道,可用其它方
式来生成哈夫曼树.)
以一棵生成的哈夫曼树为例(图片来自:http://blog.163.com/yuang_yu_ping/blog/static/4693287620098172049346/):
图片中的哈夫曼树对应的绿色即叶结点为待编码字符,对应的前缀码分别为:
A : 10
C : 01
D : 11
E : 000
M : 001
	在文本文件的压缩过程中,首先取ASCII字符对应的出现次数,然后对 0x00~0xff 这些字符根据出现频率生成对应的前缀码,然后读取
文件 ,为每一个字符进行编码.
	编码问题已经解决了,可是如果要实现文件压缩,压缩文件的编码如何进行存储呢? 首先明确一点,源文件压缩之后的编码对应的是二进制的
位段,而C语言中每个char 类型占用八位的空间,我们可以把编码后的二进制位以八位为一段,生成对应的字符来存储.对最后不足八位的二进制段,
补足八位,用一个字符表示,依次存储这些转化来的字符,即可生成压缩文件.
	另,为了解压的需要,我们要存储编码的信息至压缩文件中,可以采用源文件中各字符出现的频率来记录,解码时读取这些信息,再利用这些信息
重新生成一棵同样的哈夫曼树,另外压缩文件头中存储有最后一个字符实际占用的位数,用以在解压时对最后一个字符正确处理.
	至此,用哈夫曼编码来实现文本文件压缩的大致过程也算明白了,下面来谈一谈具体实现:
	首先是哈夫曼树的结点,
//哈夫曼树结点结构体实现
typedef struct talNode{
	
	unsigned char c;	//叶结点时对应ASCII值
	int weight;			//该结点权值
	int lt,rt;			//左、右结点下标
	
	talNode(){}
	
	talNode(unsigned char _c,int _p):
	c(_c),weight(_p),lt(-1),rt(-1)
	{}
	
	talNode(unsigned char _c,int _p,int l,int r)
		:c(_c),weight(_p),lt(l),rt(r)
	{}
	
	bool operator < (talNode a)
	{//重载运算符“<”用于二叉堆内的比较
		return weight<a.weight;
	}
	
}HuffNode; 
	生成哈夫曼树时要用到的堆
//实现二叉堆模板类,小顶堆
template <class HeapType>
class CHeap
{
	HeapType *data,tmp;
	int size;
	
	void HeapUp(int ix)
	{//自底向顶维护堆
		int f;
		for(f=F(ix);ix&&data[ix]<data[f];ix=f,f=F(f))
			SWAP(data[ix],data[f],tmp);
	}
	
	void HeapDown(int ix)
	{//自顶向底维护堆
		int l,r,t;
		HeapType min,tmp;
		if(ix>=size) return ;
		l=L(ix),r=R(ix);
		
		min=data[ix],t=ix;
		
		if(l<size&&data[l]<min)
			t=l,min=data[l];
		if(r<size&&data[r]<min)
			t=r,min=data[l];
		
		SWAP(data[ix],data[t],tmp);
		if(ix!=t) HeapDown(t);
	}
	
public:
	CHeap()
	{//这里特殊应用,堆内元素个数不超过256
		size=0;
		data=new HeapType[256];
	}
	~CHeap()
	{//释放内存
		delete data;
	}
	int getsize()
	{//返回堆大小
		return size;
	}
	void clear()
	{//清空堆
		size=0;
	}
	void insert(HeapType e)
	{//向堆尾中插入元素,并向顶维护堆
		data[size++]=e;
		HeapUp(size-1);
	}
	HeapType top()
	{//从堆顶取出元素,并向底维护堆
		HeapType ret=data[0];
		data[0]=data[--size];
		HeapDown(0);return ret;
	}
}; 
	进行压缩工作时的变量:
	HuffNode arr[512];		//哈夫曼树结点数组
	int size;				//哈夫曼树结点个数
	bool code[256][64];		//ASCII对应编码方案
	int lenth[256];			//ASCII对应编码长度

	//lastcodelenth,ps[256],用于存储压缩文件中作为文件头
	int lastcodelenth;		//文件最后一个字符实用几位
	int ps[256];			//ASCII对应出现频率
	int soucelen,targetlen; //源及目标文件长度 
	CHuffMan类的声明:
class CHuffMan{
	HuffNode arr[512];		//哈夫曼树结点数组
	int size;				//哈夫曼树结点个数
	bool code[256][64];		//ASCII对应编码方案
	int lenth[256];			//ASCII对应编码长度

	//lastcodelenth,ps[256],用于存储压缩文件中作为文件头
	int lastcodelenth;		//文件最后一个字符实用几位
	int ps[256];			//ASCII对应出现频率
	int soucelen,targetlen; //源及目标文件长度


	//私有成员函数,用于实现内部功能
	void SetHuffTree(int []);			//根据字符频率生成哈夫曼树
	void SetHuffCode(int ,bool [],int );//根据哈夫曼树生成编码方案
	void CreateHuff(int []);			//创建哈夫曼树及编码方案
	void EnCodePre(char []);			//压缩前预处理
	void DeCodePre(FILE *);				//解压前预处理
	void SaveHuffHead(FILE *);			//保存压缩文件头
	void ReadHuffHead(FILE *);			//读取压缩文件头

public:
	CHuffMan(){Clear();}				//构造函数
	~CHuffMan(){}						//析构函数
	
	//公有成员函数,用于提供使用接口
	void Clear();						//清空当前对象内容
	void EnCodeFile(char [],char []);	//编码,用于压缩文件,第一个参数为
										//源文件名,第二个参数为目标文件名
	void DeCodeFile(char [],char []);	//解码,用于解压文件,第一个参数为
										//源文件名,第二个参数为目标文件名
	void GetHuffCode();					//输出ASCII对应编码方案
	int GetSouceLen();					//输出当前工作项的源文件长度
	int GetTargetLen();					//输出当前工作项的目标文件长度
	
}; 
	思考:从运行效果上来看,压缩的比例不是非常满意,压缩率达不到25%,应该可以采用动态哈夫曼树的编码来对大文件的压缩进行优化,
从而达到更优的压缩率。确定一个初始哈夫曼树,然后根据当前处理过的字符动态的调整哈夫曼树的结构,解压过程同样根据解压过的字符调整哈夫曼树
结构。   压缩与解压的时间效率有待提高,本程序在压缩及解压过程中对硬盘进行了大量的读写,使速度受到影响,可以通过缓冲带还实现提速,即构
造内存缓冲区,每次读写多个字符,既而进行处理。
	
	(注:根据哈夫曼树的构成原理,可以预见一种特殊的情况:哈夫曼树的结构为每一层只有一个叶结点,这种情况下的每个字符对应的前缀码
长度最高会达到256位,在此程序中未进行特殊考虑。)
	运行效果:
哈夫曼编码及文本文件的压缩解压(c++SourceCode)_第1张图片
哈夫曼编码及文本文件的压缩解压(c++SourceCode)_第2张图片
哈夫曼编码及文本文件的压缩解压(c++SourceCode)_第3张图片
哈夫曼编码及文本文件的压缩解压(c++SourceCode)_第4张图片
哈夫曼编码及文本文件的压缩解压(c++SourceCode)_第5张图片
	
	源代码:

 /***************************************************************** Title: 哈夫曼编码及文件压缩 Author: Dooler IDE: VC++6.0 Date: 2011-5-31 INFO: 实现了两个类, CHeap(二叉堆) CHuffMan(文件压缩) 可对文本文件进行压缩 QQ: 441809866 ******************************************************************/ #include<iostream> #define ASCIIL 256 #define F(x) ((x-1)>>1) #define L(x) ((x<<1)+1) #define R(x) ((x<<1)+2) #define SWAP(a,b,tmp) {tmp=a;a=b;b=tmp;} using namespace std; //实现二叉堆模板类,小顶堆 template <class HeapType> class CHeap { HeapType *data,tmp; int size; void HeapUp(int ix) {//自底向顶维护堆 int f; for(f=F(ix);ix&&data[ix]<data[f];ix=f,f=F(f)) SWAP(data[ix],data[f],tmp); } void HeapDown(int ix) {//自顶向底维护堆 int l,r,t; HeapType min,tmp; if(ix>=size) return ; l=L(ix),r=R(ix); min=data[ix],t=ix; if(l<size&&data[l]<min) t=l,min=data[l]; if(r<size&&data[r]<min) t=r,min=data[l]; SWAP(data[ix],data[t],tmp); if(ix!=t) HeapDown(t); } public: CHeap() {//这里特殊应用,堆内元素个数不超过256 size=0; data=new HeapType[256]; } ~CHeap() {//释放内存 delete [] data; } int getsize() {//返回堆大小 return size; } void clear() {//清空堆 size=0; } void insert(HeapType e) {//向堆尾中插入元素,并向顶维护堆 data[size++]=e; HeapUp(size-1); } HeapType top() {//从堆顶取出元素,并向底维护堆 HeapType ret=data[0]; data[0]=data[--size]; HeapDown(0);return ret; } }; //哈夫曼树结点结构体实现 typedef struct talNode{ unsigned char c; //叶结点时对应ASCII值 int weight; //该结点权值 int lt,rt; //左、右结点下标 talNode(){} talNode(unsigned char _c,int _p): c(_c),weight(_p),lt(-1),rt(-1) {} talNode(unsigned char _c,int _p,int l,int r) :c(_c),weight(_p),lt(l),rt(r) {} bool operator < (talNode a) {//重载运算符“<”用于二叉堆内的比较 return weight<a.weight; } }HuffNode; //哈夫曼文件压缩类声明 /************************************************ 类说明: 1、通过EnCodeFile(char filef[],char filet[]) 和DeCodeFile(char filef[],char filet[]) 来建立工作项,此时对象中保存压缩或解压过和中的 详细信息 2、通过GetHuffCode();GetSouceLen();GetTargetLen() 来获得当前工作项信息 3、通过Clear();清除当前工作项信息,为下一次压缩或 解压作准备 ************************************************/ class CHuffMan{ HuffNode arr[512]; //哈夫曼树结点数组 int size; //哈夫曼树结点个数 bool code[256][64]; //ASCII对应编码方案 int lenth[256]; //ASCII对应编码长度 //lastcodelenth,ps[256],用于存储压缩文件中作为文件头 int lastcodelenth; //文件最后一个字符实用几位 int ps[256]; //ASCII对应出现频率 int soucelen,targetlen; //源及目标文件长度 //私有成员函数,用于实现内部功能 void SetHuffTree(int []); //根据字符频率生成哈夫曼树 void SetHuffCode(int ,bool [],int );//根据哈夫曼树生成编码方案 void CreateHuff(int []); //创建哈夫曼树及编码方案 void EnCodePre(char []); //压缩前预处理 void DeCodePre(FILE *); //解压前预处理 void SaveHuffHead(FILE *); //保存压缩文件头 void ReadHuffHead(FILE *); //读取压缩文件头 public: CHuffMan(){Clear();} //构造函数 ~CHuffMan(){} //析构函数 //公有成员函数,用于提供使用接口 void Clear(); //清空当前对象内容 void EnCodeFile(char [],char []); //编码,用于压缩文件,第一个参数为 //源文件名,第二个参数为目标文件名 void DeCodeFile(char [],char []); //解码,用于解压文件,第一个参数为 //源文件名,第二个参数为目标文件名 void GetHuffCode(); //输出ASCII对应编码方案 int GetSouceLen(); //输出当前工作项的源文件长度 int GetTargetLen(); //输出当前工作项的目标文件长度 }; void CHuffMan::SetHuffTree(int ps[]) { //每次取出两权值最小的结点合并成新树, //加入堆,直至堆中只余有一个元素 CHeap<HuffNode> hp; //二叉堆对象 for(int i=0;i<ASCIIL;i++){ //如果字符i出现过,则插入二插堆 if(ps[i]) hp.insert(HuffNode(i,ps[i])); } size=0; //初始化哈夫曼树中结点个数 while(hp.getsize()>1){ arr[size++]=hp.top(); //取出权值最小的两个结点 arr[size++]=hp.top(); hp.insert(HuffNode(0, arr[size-1].weight+arr[size-2].weight, size-1,size-2)); //合并结点,并插入堆 } arr[size++]=hp.top(); //arr[size-1]为哈夫曼树根 } void CHuffMan::SetHuffCode(int ix,bool stk[],int top) { //递归深搜哈夫曼树,生成所有存在的ASCII //的前缀码 if(arr[ix].c){ //如果 if(top){ //此判断用于只含有一类字符的文件 memmove(code[arr[ix].c],stk,sizeof(bool)*top); lenth[arr[ix].c]=top; } else lenth[arr[ix].c]=1; return ; } stk[top]=0; //左子树的边设为0 SetHuffCode(arr[ix].lt,stk,top+1); //递归进入左子树 stk[top]=1; //右子树的边设为1 SetHuffCode(arr[ix].rt,stk,top+1); //递归进入右子树 } void CHuffMan::CreateHuff(int ps[]) { //构造哈夫曼树及前缀码 bool stk[64]; SetHuffTree(ps); //根据字符频率构造哈夫曼树 SetHuffCode(size-1,stk,0); //根据哈夫曼树生成前缀码 } void CHuffMan::EnCodePre(char sfilename[]) { //压缩文件预处理,读取字符出现频率 FILE *fp; //及构造哈夫曼树及前缀码 int c; fp=fopen(sfilename,"rb"); if(fp==NULL){ cout<<"读取文件错误"<<endl; exit(0); } memset(ps,0,sizeof(ps)); //读取字符出现频率 while(true){ c=fgetc(fp); if(feof(fp))break; ps[c]++; } fclose(fp); CreateHuff(ps); //构造哈夫曼树及前缀码 } void CHuffMan::DeCodePre(FILE *fp) { //解压文件预处理,读取压缩文件头 //根据读取头信息构千哈夫曼树及前缀码 ReadHuffHead(fp); CreateHuff(ps); } void CHuffMan::SaveHuffHead(FILE *fp) { //向压缩文件中写文件头 fwrite((void *)&lastcodelenth,4,257,fp);//从lastcodelenth的地址开始的连续 //4*257个字节,即lastcodelenth和 //ps[256]数组内容 targetlen+=4*257; } void CHuffMan::ReadHuffHead(FILE *fp) { //从缩文件中读文件头 fread((void *)&lastcodelenth,4,257,fp); //从lastcodelenth的地址开始的连续 //4*257个字节,即lastcodelenth和 soucelen+=4*257; //ps[256]数组内容 } void CHuffMan::Clear() { //清空前前工作项 size=0;soucelen=targetlen=0; lastcodelenth=0; memset(lenth,0,sizeof(lenth)); memset(ps,0,sizeof(ps)); } int CHuffMan::GetSouceLen() { //获取当前工作项的源文件长度 return soucelen; } int CHuffMan::GetTargetLen() { //获取当前工作项的目标文件长度 return targetlen; } void CHuffMan::GetHuffCode() { //输出当前工作项的编码前缀码方案 int i; for(i=0;i<ASCIIL;i++){ if(lenth[i]>0){ //如果前缀码不空 printf("%c : ",i); //输出ASCII码 for(int j=0;j<lenth[i];j++){ printf("%d",code[i][j]); //输出对应前缀码 } puts(""); } } } void CHuffMan::EnCodeFile(char sfilename[],char gfilename[]) { //将文件sfilename //压缩为文件gfilename[] FILE *fp,*fpt; int c,data,l,i; EnCodePre(sfilename); //压缩预处理,生成哈曼树及 //字符前缀码 fp=fopen(sfilename,"rb"); fpt=fopen(gfilename,"wb"); SaveHuffHead(fpt); //写入压缩文件的头信息 //!!!注意,此时lastcodelenth //为空,需压缩结束时重置 l=data=0; puts("Encoding ... "); //编码压缩过程,依次对源文件字符进行编码 while(true){ //存入一个字符中,用移位操作实现,每8位前 c=fgetc(fp); //缀码对应一个字符,将该字符存入目标文件, if(feof(fp)) break; //最终不足8位的记录最后一位占用的前缀码长度 soucelen++; //源文件长度增加 for(i=0;i<lenth[c];i++){ //对data进行左移,空出最低位 data<<=1; //对当前字符的前缀码当前们存储于data中 data+=code[c][i]; if(++l%8==0){ //满8位,则存储 fputc(data,fpt); targetlen++; //目标文件长度增加 } } } //对最后的一个字符进行处理 lastcodelenth=l%8; //记录实际占用位的长度 data<<=8-lastcodelenth; //空出剩余位 fputc(data,fpt); //输出至文件 targetlen++; //目标文件长度增加 fseek(fpt,0,SEEK_SET); //!!!回溯至文件头,更新lastcodelenth至 fwrite(&lastcodelenth,4,1,fpt); //真实值 fclose(fp); //关闭文件 fclose(fpt); } void CHuffMan::DeCodeFile(char sfilename[],char gfilename[]) { //解压文件sfile至gfile FILE *fp=fopen(sfilename,"rb"); FILE *fpt=fopen(gfilename,"wb"); int c,t,l,i; //l用于记录当前前缀码段的长度 HuffNode cur; bool tmp[64]; //tmp[]用于记录当前的前缀码段 DeCodePre(fp); l=0; puts("Decoding ... "); fscanf(fp,"%c",&c); //解码过程,压缩过程的逆过程,取出编码了的字符, //按位取出,存于tmp[]中,找出前缀码对应的字符 while(!feof(fp)){ soucelen++; fscanf(fp,"%c",&t); if(feof(fp))break; for(i=l+7;i>=l;i--){ //按位取出前缀码 tmp[i]=c&1;c>>=1; }l+=8; while(l>=32){ //如果当前前缀码段超出一定的长度,则取出前缀码 //进行解码 for(i=0,cur=arr[size-1];!cur.c;i++) cur=tmp[i]?arr[cur.rt]:arr[cur.lt];//找到前缀码段对应第一个字符 fprintf(fpt,"%c",cur.c); //输出至目标文件 l-=i;targetlen++; //前缀码段减去当前字符前缀码长度 memmove(tmp,tmp+i,sizeof(bool)*l); //数组顺移至开头,即从0开始记录当前的 //前缀码段 }c=t; } for(i=l+7;i>=l;i--){ //对最后一个字符做特殊处理 tmp[i]=c&1; //取出每一位 c>>=1; } l+=lastcodelenth; //只利用最后一个字符的前lastcodelenth位 while(l){ //输出剩余的前缀码段对应的字符 for(i=0,cur=arr[size-1];!cur.c;i++) cur=tmp[i]?arr[cur.rt]:arr[cur.lt]; fprintf(fpt,"%c",cur.c);l-=i;targetlen++; memmove(tmp,tmp+i,sizeof(bool)*l); } fclose(fp);fclose(fpt); //关闭文件 } bool Menu(int &op) { system("cls"); printf("|/t哈夫曼编码实现文件压缩/t|/n"); printf("功能:/n"); printf("_________________________________/n"); printf("|/t1、/t压缩文件/t|/n"); printf("|/t2、/t解压文件/t|/n"); printf("|/t3、/t输出编码方案/t|/n"); printf("|/t0、/t退出 /t|/n"); printf("---------------------------------/n"); do{ printf("请选择:"); scanf("%d",&op); }while(op<0||op>3); return op?true:false; } int main() { int op; char file1[32],file2[32]; CHuffMan work; char step[2]; while(Menu(op)){ switch(op){ case 1: printf("请输入待压缩文件名(.txt):"); scanf("%s",file1); printf("请输入压缩文件名(.huf):"); scanf("%s",file2); work.Clear(); work.EnCodeFile(file1,file2); printf("源文件长度:/t%d/n",work.GetSouceLen()); printf("目标文件长度:/t%d/n",work.GetTargetLen()); break; case 2: printf("请输入待压缩文件名(.huf):"); scanf("%s",file1); printf("请输入压缩文件名(.txt):"); scanf("%s",file2); work.Clear(); work.DeCodeFile(file1,file2); printf("源文件长度:/t%d/n",work.GetSouceLen()); printf("目标文件长度:/t%d/n",work.GetTargetLen()); break; case 3: work.GetHuffCode(); break; } puts("按任意键继续..."); gets(step); gets(step); } return 0; }

你可能感兴趣的:(C++,C++,工作,File,File,存储,FP)