白话 哈夫曼 压缩 解压缩,包你懂啊
好久没更新博客,每一次的跟新也是对自己所学的知识一次总结
所以今次依旧保持原来的风格,白话化,今天要讲的是文件的压缩和解压缩
学过数据结构的读者应该知道,是用哈夫曼的方法去做,但是往往我们学了个
大概就没学下去,很快就忘了,所以今天想这个知识总结下,做一个完整的例子
好吧,那就开始吧
首先要大家明白一个概念,为什么文件能够压缩,就好像有mp3啊,视频啊,图片啊,这几种我没研究
过,但如果要到现实上说的话我们通常把多余的空隙去掉,比如你去送一车货,你肯定想装多一点货
但如果货物时不是正方形的,如果是三角形的形状的话或者其他形状的话那就很难压缩了,在货物不
变形的情况下,所以很多时候是尽量把空出来的的一些位置放上其他小东西,这样就不会太浪费啦
好了,回到我们今天所说的话题,文件文本压缩,一般来说大家应该知道,一个字符时用8个字节表示的
那么针对一些出现的字符00000010,前面这么多的0就是浪费,也不能说浪费,怎么说呢,都是举个例子吧
比如一个文本的内容
AAGGDCCCC
就这么几个字符,有人立即就会想到,就这么几个字符,不用8位去表示一个字符吧,其实是对的,就只有
4种类型,用两位表示就可以啦, A 为00,G为01,d为10,c为11, 就这样,不就可以吗
对,是可以的,压缩后 的文件可以这样表示(空格忽略)
00 00 01 01 10 11 11 11 11
A A G G D C C C C
总共才2个字节多一点(22bits),原来的大小是10个字节,压缩率也不错,是吧
但别开心的这么早,那么我们收到这个压缩过的文件怎么解压缩呢,因为你要把 00 还原成
A的asscii码才能显示啊,所以就这样的话事不行的,需要在压缩的文件追加一些信息提供解压缩用的
那我们定义一个结构吧
struct node
{
char b //表示代表的字符
char bits[2] //表示编码后的位数
};
那么追加后的信息为 0x41 0x00 0x00 0x47 0x00 0x01 .........
其他就不写啦
如果这样我们看看能不能解压缩,
我们先初始化结构
fread(&node[i],sizeof(struct node)1,fp);
其实这里必须要知道从哪里开始读取结构数据,要知道这个,必须知道压缩后的长度是多小,再移动文件指针去读取
好了,首先我们预先知道每个字符时用2个字符编码的,每当我们读取一个字节出来的时候,我们就要对它解码
再写入新的文件
比如 00 00 01 01 我们读出的第一个字节是这样的
先 itoa(f,buf,2); 把二进制转换成字符就是"00000101"这样,注意二进制00 00 01 01和"00000101"
是不同的,前者占一个字节,后者占8个字节,不懂的去看下asccii表字符串的定义
再循环我们的结构通过 比较
memcmp(node[i].bits,buf,2)==0 就知道所代表的对的字符,然后写进文件就可以啦
比如第一个00 对应结构的bits 的A,然后把A写进去就可以啦
到了这里,我们是否考虑下呢 压缩率
我们为了可以解压缩,加多了12个字节 (4*3),压缩率大大降下,我们是否应该想下其他办法呢,从另外一个角度看
这样的压缩式没有什么意思的,比如文件 一旦字符的种类个数每个字符都出现过,那还是要8位编码去表示
每一个字符
例如文件的内容是 aa bb cc dd..................zz,还有一些不可打印的字符,例如回车换行符啊,所以要想做到
以不变应万变的话,上面的程序是不可行的,只因它还是定长编码,那有没有其他方法啦,是有的那就是变长
编码,主角哈夫曼编码要出现啦,这个东西在上面的基础作了进一步的改进,就是他是根据字符的出现频率
来确定该字符的编码长度,出现越高频率的字符编码越短,这样才能减小文件大小啊,大家都懂吧
这样的话还有一个条件,就是两两编码字符间不可以一个是另一个的前序,不然会解码错误
memcmp(node[i].bits,buf,2)==0,大家可以根据这个来参悟,哈夫曼树两个就符合了,其实他也是二叉树的一种
下面把他的结构列出来
struct head
{
unsigned char b; //记录字符在数组中的位置
long count; //字符出现频率(权值)
long parent,lch,rch; //定义哈夫曼树指针变量 父亲,左儿子,右儿子 指针
char bits[256]; //定义存储哈夫曼编码的数组
}
这里为什么不用指针呢,是为了方便我们的程序,就那我们的上面的例子做例子
A A G G D C C C C
A(2) C(4) D(1) G(2)
那么哈夫慢树就是这样
(9)
(5) C(4 编码 0)
G(2 编码 11) (3)
D(1 编码 101) A(2 编码 100)
好了,我们就计算一下压缩率 A = 3*2 = 6; D = 3*1 = 3 G = 2*2 =4 C = 1*4 =4
17bits ,也就是3个字节,这根本没什么,最后那1bit 要补0,比上面还小啊,所以这算法就是牛,好了
所有知识都差不多讲完啦,开始我们的程序之旅吧,解压缩思想跟上面的差不多有一点不同的是
注意(哈夫曼编码不是字符串存入的,是合成存储的,不明的留意给我)
压缩:
1,定义哈夫曼树结构和生成哈夫曼程序,
2,哈夫曼编码,
3,循环把源文件进行哈夫曼编码存进新的文件
4,吧哈夫曼结构有用的信息追加文件后面,解码用的
解压缩的
1读入文件压缩文件长度,移动文件指针
2,还原哈夫曼结构
3,移动文件指针,依次读入压缩字节进行解码,比较哈夫曼编码
4,吧字节写进新文件里,
5解压缩成功
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <conio.h> struct head { unsigned char b; //记录字符在数组中的位置 long count; //字符出现频率(权值) long parent,lch,rch; //定义哈夫曼树指针变量 char bits[256]; //定义存储哈夫曼编码的数组 } header[512],tmp; /*压缩*/ void compress() { char filename[255],outputfile[255],buf[512]; unsigned char c; long i,j,m,n,f; long min1,pt1,flength,length1,length2; double div; FILE *ifp,*ofp; printf("\t请您输入需要压缩的文件:"); gets(filename); ifp=fopen(filename,"rb"); if(ifp==NULL) { printf("\n\t文件打开失败!\n\n"); return; } printf("\t请您输入压缩后的文件名:"); gets(outputfile); ofp=fopen(strcat(outputfile,".hub"),"wb"); if(ofp==NULL) { printf("\n\t压缩文件失败!\n\n"); return; } flength=0; while(!feof(ifp)) { fread(&c,1,1,ifp); header[c].count++; //字符重复出现频率+1 flength++; //字符出现原文件长度+1 } flength--; length1=flength; //原文件长度用作求压缩率的分母 header[c].count--; for(i=0;i<512;i++) { if(header[i].count!=0) header[i].b=(unsigned char)i; /*将每个哈夫曼码值及其对应的ASCII码存放在一维数组header[i]中, 且编码表中的下标和ASCII码满足顺序存放关系*/ else header[i].b=0; header[i].parent=-1;header[i].lch=header[i].rch=-1; //对结点进行初始化 } for(i=0;i<256;i++) //根据频率(权值)大小,对结点进行排序,选择较小的结点进树 { for(j=i+1;j<256;j++) //这里运用冒泡排序法啊 { if(header[i].count<header[j].count) { tmp=header[i]; header[i]=header[j]; header[j]=tmp; } } } for(i=0;i<256;i++) if(header[i].count==0) break; n=i; //外部叶子结点数为n个时,内部结点数为n-1,整个哈夫曼树的需要的结点数为2*n-1. m=2*n-1; for(i=n;i<m;i++) //构建哈夫曼树 { min1=999999999; //预设的最大权值,即结点出现的最大次数 for(j=0;j<i;j++) { if(header[j].parent!=-1) continue; //parent!=-1说明该结点已存在哈夫曼树中,跳出循环重新选择新结点*/ if(min1>header[j].count) { pt1=j; min1=header[j].count; continue; } } //上面已经取出最小的 header[i].count=header[pt1].count; header[pt1].parent=i; //依据parent域值(结点层数)确定树中结点之间的关系 header[i].lch=pt1; //计算左分支权值大小 min1=999999999; for(j=0;j<i;j++) //这里运用选择排序法啊 { if(header[j].parent!=-1) continue; if(min1>header[j].count) { pt1=j; min1=header[j].count; continue; } } header[i].count+=header[pt1].count; header[i].rch=pt1; //计算右分支权值大小 header[pt1].parent=i; } for(i=0;i<n;i++) //哈夫曼无重复前缀编码 { f=i; header[i].bits[0]=0; //根结点编码0 while(header[f].parent!=-1) { j=f; //也就是i f=header[f].parent; //d 12 if(header[f].lch==j) //置左分支编码0 { j=strlen(header[i].bits); memmove(header[i].bits+1,header[i].bits,j+1); //依次存储连接“0”“1”编码 header[i].bits[0]='0'; } else //置右分支编码1 { j=strlen(header[i].bits); memmove(header[i].bits+1,header[i].bits,j+1); header[i].bits[0]='1'; } } } fseek(ifp,0,SEEK_SET); //从文件开始位置向前移动0字节,即定位到文件开始位置 fwrite(&flength,sizeof(int),1,ofp); /*用来将数据写入文件流中,参数flength指向欲写入的数据地址, 总共写入的字符数以参数size*int来决定,返回实际写入的int数目1*/ fseek(ofp,8,SEEK_SET); buf[0]=0; //定义缓冲区,它的二进制表示00000000 f=0; pt1=8; /*假设原文件第一个字符是"A",8位2进制为01000001,编码后为0110识别编码第一个'0', 那么我们就可以将其左移一位,看起来没什么变化。下一个是'1',应该|1,结果00000001 同理4位都做完,应该是00000110,由于字节中的8位并没有全部用完,我们应该继续读下一个字符, 根据编码表继续拼完剩下的4位,如果字符的编码不足4位,还要继续读一个字符, 如果字符编码超过4位,那么我们将把剩下的位信息拼接到一个新的字节里*/ while(!feof(ifp)) { c=fgetc(ifp); f++; for(i=0;i<n;i++) { if(c==header[i].b) break; } strcat(buf,header[i].bits); j=strlen(buf); c=0; while(j>=8) //对哈夫曼编码位操作进行压缩存储 //23.255814 { for(i=0;i<8;i++) { if(buf[i]=='1') c=(c<<1)|1; else c=c<<1; } fwrite(&c,1,1,ofp); pt1++; //统计压缩后文件的长度 strcpy(buf,buf+8); //一个字节一个字节拼接 j=strlen(buf); } if(f==flength) break; } if(j>0) //对哈夫曼编码位操作进行压缩存储 { strcat(buf,"00000000"); for(i=0;i<8;i++) { if(buf[i]=='1') c=(c<<1)|1; else c=c<<1; } fwrite(&c,1,1,ofp); pt1++; } fseek(ofp,4,SEEK_SET); //移动文件指针位置到第四位 fwrite(&pt1,sizeof(long),1,ofp); //写入统计压缩后文件的长度 fseek(ofp,pt1,SEEK_SET); //移动文件指针到压缩后文件的长度后的位置 fwrite(&n,sizeof(long),1,ofp); //写入节点数目 为13 for(i=0;i<n;i++) { fwrite(&(header[i].b),1,1,ofp); //写入每个节点的代表的字符 c=strlen(header[i].bits); fwrite(&c,1,1,ofp); //写入每个字符哈夫曼编码的长度 j=strlen(header[i].bits); //统计哈夫曼长度 if(j%8!=0) //若存储的位数不是8的倍数,则补0 { for(f=j%8;f<8;f++) //比如长度只为3 //比如 9 strcat(header[i].bits,"0"); //011 00000 01010101 10000000 } while(header[i].bits[0]!=0) //这里检查是否到了字符串末尾 { c=0; for(j=0;j<8;j++) //字符的有效存储不超过8位,则对有效位数左移实现两字符编码的连接 { if(header[i].bits[j]=='1') c=(c<<1)|1; //|1不改变原位置上的“0”“1”值 else c=c<<1; } strcpy(header[i].bits,header[i].bits+8); //把字符的编码按原先存储顺序连接 fwrite(&c,1,1,ofp); } } length2=pt1--; div=((double)length1-(double)length2)/(double)length1; //计算文件的压缩率 fclose(ifp); fclose(ofp); printf("\n\t压缩文件成功!\n"); printf("\t压缩率为 %f%%\n\n",div*100); return; } /*解压缩*/ void uncompress() { char filename[255],outputfile[255],buf[255],bx[255]; unsigned char c; long i,j,m,n,f,p,l; long flength; FILE *ifp,*ofp; printf("\t请您输入需要解压缩的文件:"); gets(filename); ifp=fopen(strcat(filename,".hub"),"rb"); if(ifp==NULL) { printf("\n\t文件打开失败!\n"); return; } printf("\t请您输入解压缩后的文件名:"); gets(outputfile); ofp=fopen(outputfile,"wb"); if(ofp==NULL) { printf("\n\t解压缩文件失败!\n"); return; } fread(&flength,sizeof(long),1,ifp); //读取原文件长度,对文件进行定位 fread(&f,sizeof(long),1,ifp); //压缩长度 fseek(ifp,f,SEEK_SET); //长度后的节点数目 fread(&n,sizeof(long),1,ifp); //节点数 for(i=0;i<n;i++) { fread(&header[i].b,1,1,ifp); //字符值 fread(&c,1,1,ifp); p=(long)c; //读取原文件字符的权值 header[i].count=p; //哈夫曼的编码长度 header[i].bits[0]=0; if(p%8>0) m=p/8+1; //字节数 else m=p/8; //m=1 for(j=0;j<m;j++) { fread(&c,1,1,ifp); f=c; itoa(f,buf,2); //将f转换为二进制表示的字符串 f=strlen(buf); for(l=8;l>f;l--) { strcat(header[i].bits,"0"); } strcat(header[i].bits,buf); } header[i].bits[p]=0; } for(i=0;i<n;i++) //根据哈夫曼编码的长短,对结点进行排序(从小到大啊) { for(j=i+1;j<n;j++) //冒泡排序 { if(strlen(header[i].bits)>strlen(header[j].bits)) { tmp=header[i]; header[i]=header[j]; header[j]=tmp; } } } p=strlen(header[n-1].bits); //最大长度 fseek(ifp,8,SEEK_SET); //移动文件指针 m=0; bx[0]=0; while(1) //通过哈夫曼编码的长短,依次解码,从原来的位存储还原到字节存储 { while(strlen(bx)<(unsigned int)p) { fread(&c,1,1,ifp); f=c; itoa(f,buf,2); f=strlen(buf); for(l=8;l>f;l--) //在单字节内对相应位置补0 { strcat(bx,"0"); } strcat(bx,buf); } for(i=0;i<n;i++) { if(memcmp(header[i].bits,bx,header[i].count)==0) break; } strcpy(bx,bx+header[i].count); /*从压缩文件中的按位存储还原到按字节存储字符, 字符位置不改变*/ c=header[i].b; fwrite(&c,1,1,ofp); m++; //统计解压缩后文件的长度 if(m==flength) break; //flength是原文件长度 } fclose(ifp); fclose(ofp); printf("\n\t解压缩文件成功!\n"); if(m==flength) //对解压缩后文件和原文件相同性比较进行判断(根据文件大小) printf("\t解压缩文件与原文件相同!\n\n"); else printf("\t解压缩文件与原文件不同!\n\n"); return; } /*主函数*/ int main() { int c; while(1) //菜单工具栏 { printf("\t _______________________________________________\n"); printf("\n"); printf("\t * 压缩、解压缩 小工具 * \n"); printf("\t _______________________________________________\n"); printf("\t _______________________________________________\n"); printf("\t| |\n"); printf("\t| 1.压缩 |\n"); printf("\t| 2.解压缩 |\n"); printf("\t| 0.退出 |\n"); printf("\t|_______________________________________________|\n"); printf("\n"); printf("\t 说明:(1)采用哈夫曼编码\n"); printf("\t (2)适用于字符型文本文件\n"); printf("\n"); do //对用户输入进行容错处理 { printf("\n\t*请选择相应功能(0-2):"); c=getch(); printf("%c\n",c); if(c!='0' && c!='1' && c!='2') { printf("\t@_@请检查您输入的数字在0~2之间!\n"); printf("\t请再输入一遍!\n"); } }while(c!='0' && c!='1' && c!='2'); if(c=='1') compress(); //调用压缩子函数 else if(c=='2') uncompress(); //调用解压缩子函数 else { printf("\t欢迎您再次使用该工具^_^\n"); exit(0); //退出该工具 } system("pause"); //任意键继续 system("cls"); //清屏 } return 0; }

浙公网安备 33010602011771号