哈夫曼编码是一种编码方式,是可变字长编码(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生成哈夫曼树时要用到的堆
//实现二叉堆模板类,小顶堆 templateclass CHeap { HeapType *data,tmp; int size; void HeapUp(int ix) {//自底向顶维护堆 int f; for(f=F(ix);ix&&data[ix]=size) return ; l=L(ix),r=R(ix); min=data[ix],t=ix; if(l 进行压缩工作时的变量:
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位,在此程序中未进行特殊考虑。)运行效果:
源代码:
/***************************************************************** Title: 哈夫曼编码及文件压缩 Author: Dooler IDE: VC++6.0 Date: 2011-5-31 INFO: 实现了两个类, CHeap(二叉堆) CHuffMan(文件压缩) 可对文本文件进行压缩 QQ: 441809866 ******************************************************************/ #include
#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 CHeap { HeapType *data,tmp; int size; void HeapUp(int ix) {//自底向顶维护堆 int f; for(f=F(ix);ix&&data[ix]=size) return ; l=L(ix),r=R(ix); min=data[ix],t=ix; if(l hp; //二叉堆对象 for(int i=0;i 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<<"读取文件错误"< 0){ //如果前缀码不空 printf("%c : ",i); //输出ASCII码 for(int j=0;j =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; }