白话 哈夫曼 压缩 解压缩,包你懂啊


好久没更新博客,每一次的跟新也是对自己所学的知识一次总结
所以今次依旧保持原来的风格,白话化,今天要讲的是文件的压缩和解压缩
学过数据结构的读者应该知道,是用哈夫曼的方法去做,但是往往我们学了个
大概就没学下去,很快就忘了,所以今天想这个知识总结下,做一个完整的例子
好吧,那就开始吧
首先要大家明白一个概念,为什么文件能够压缩,就好像有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;
}
posted @ 2012-10-21 22:35  见吻戏哦  阅读(2121)  评论(1)    收藏  举报