哈夫曼编码压缩解压缩.docx
《哈夫曼编码压缩解压缩.docx》由会员分享,可在线阅读,更多相关《哈夫曼编码压缩解压缩.docx(26页珍藏版)》请在冰豆网上搜索。
哈夫曼编码压缩解压缩
哈夫曼编码压缩解压缩
xx学院
课程设计题目哈夫曼编码压缩解压缩
院系名称计算机科学与技术系
专业(班级)
姓名(学号)
指导教师
完成时间
1
哈夫曼编码实现文本文件的压缩和解压缩一、任务分析和定义
在了解哈夫曼编码压缩解压缩原理之前,首先让我们来认识哈夫曼树和哈夫曼编码。
1.在了解哈夫曼编码压缩解压缩原理之前,首先让我们来认识哈夫曼树。
哈夫曼树又称最
优二叉树,是带权路径长度最小的二叉树。
2.在文本文件中多采用二进制编码。
为了使文件尽可能的缩短,可以对文件中每个字符出
现的次数进行统计。
设法让出现次数多的字符二进制码短些,而让那些很少出现的字符
二进制码长一些。
若对字符集进行不等长编码,则要求字符集中任一字符的编码都不是
其它字符编码的前缀。
为了确保哈夫曼编码的唯一性,我们可以对它的左右子树的大小
给予比较限定,如:
左子树的权值小于右子树的权值。
哈夫曼树中的左右分支各代表‘0’
和‘1’,则从根节点到叶子节点所经历的路径分支上的‘0’或‘1’组成的字符串,为
该节点对应字符的哈夫曼编码。
3.统计字符集中每个字符在文件中出现的平均概率(概率越大,要求编码越短)。
利用哈
夫曼树的特点:
权越大的叶子离根越近,将每个字符的概率值作为权值,构造哈夫曼树。
则概率越大的结点,路径越短。
哈夫曼译码是从二进制序列的头部开始,顺序匹配成共
的部分替换成相应的字符,直至二进制转换为字符序列。
4.哈夫曼用于文件解压缩的基础是在压缩二进制代码的同时还必须存储相应的编码,这样
就可以根据存储的哈夫曼编码对压缩代码进行解压缩。
总之,该课题的任务应该是首先要打开要压缩的文本文件并读出其中字符出现的频率,以其
为权值构建哈夫曼树。
其次要找到构建压缩功能的方法,在构建哈夫曼树的基础上进行编码,
改变字符原先的存储结构,以达到压缩文件的目的,此外还要存储相应的哈夫曼编码,为解
压缩做准备。
二、概要设计和数据结构的选择
以下是在任务分析及对题意的理解做出的概要设计和对数据结构的选择。
1.数据结构定义:
structhead
{
unsignedcharb;//记录字符在数组中的位置
longcount;//字符出现频率(权值)
longparent,lch,rch;//定义哈夫曼树指针变量
charbits[256];//定义存储哈夫曼编码的数组}
header[512],tmp;
2.定义所需要的子函数:
voidcompress()//用于实现文件的压缩功能
voiduncompress()//用于实现文件的解压缩功能3.主程序的流程及程序模块间的关系:
主函数实现菜单工具栏,并且能够对用户的输入进行容错处理,通过if语句实现该工具的两个子功能:
即:
a.调用压缩子函数压缩同文件夹下的字符型文本文件
b.调用解压缩子函数解压缩同文件夹下的字符型文本文件并可在完成相应功能后安全退出
2
压缩或解压缩的文件在同文件夹下生成
4.流程图:
主函数
压缩函数compress解压缩函数菜单uncompress
退出
图1.程序模块
打开文本文件,统计文件长度
初始化结点
根据权值大小结点排序
选择最小权值两个结点
Parent=-1Parent!
=-1构建哈夫曼树
计算左右分支权值大小,进行无重复前缀编码
哈夫曼编码位操作压缩存储
NO压缩文件成功!
YES压缩文件失败!
压缩率为%
图2.压缩函数
3
打开压缩文件读取原文件长度进行文件定位
根据哈夫曼编码的长短,对结点进行排序
通过哈夫曼编码的长短,依次解码,从原来的位存储还原到字节存储
在单字节内对相应位置补0
YES
NOYES
解压缩文件成功!
解压缩文件失败!
对解压缩后文件和原文件相同性比较进行判断
图3.解压缩函数
二、详细设计和编码
以下是主函数和子函数的详细设计和附部分源代码。
1.压缩(compress)子函数设计
该函数主要是为了实现对同文件夹下文本文件的压缩。
1.1算法中先是统计文件中字符重复出现的频率和文件的长度。
字符出现的频率用作结点
的权值,而原文件长度用作求压缩率的分母。
1.2然后对结点进行初始化,根据频率(权值)大小,对结点进行排序,依次选择权值较
小且parent域不等于-1的结点建哈夫曼树。
计算左右孩子权值大小,与后续进入树的结点
权值进行比较,从而确定后续结点在树中的位置。
1.3进行哈夫曼无重复前缀编码,再对哈夫曼编码进行位操作从原先的按字节存储到现在
的按位存储实现字符存储空间的压缩。
假设原文件第一个字符是“A”,8位2进制为01000001,
编码后为0110识别编码第一个‘0’,那么我们就可以将其左移一位,看起来没什么变化。
下一个是‘1’,应该|1,结果00000001,同理4位都做完,应该是00000110,由于字节中的8位并没有全部用完,我们应该继续读下一个字符,根据编码表继续拼完剩下的4位,如果字符的编码不足4位,还要继续读一个字符,如果字符编码超过4位,那么我们将把剩下
4
的位信息拼接到一个新的字节里。
其中编码后压缩字符存储空间从字节到位代码实现如下:
for(i=0;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++)
strcat(header[i].bits,"0");
}
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”值
elsec=c<<1;
}
strcpy(header[i].bits,header[i].bits+8);//把字符的编码按原先存
储顺序连接
fwrite(&c,1,1,ofp);
}
}
1.4统计压缩了的文件长度与原文件长度作比,计算压缩率。
2.解压缩(uncompress)子函数设计
该函数主要是为了实现对同文件夹下压缩文件的解压缩。
2.1压缩执行程序,完成后输出压缩的结果,并且保留压缩后的二进制代码和哈夫曼编码
为解压缩做准备;在执行解压缩时,首先调用压缩所产生的二进制代码和哈夫曼编码,以出
现的字符为叶子节点,以字符出现的频率为权值构建新的哈夫曼树,它的输出结果就是解压
缩后的文件。
其中解码过程如下:
while
(1)//通过哈夫曼编码的长短,依次解码,从原来的位存储还原到字节存储
{
while(strlen(bx)<(unsignedint)p)
{
fread(&c,1,1,ifp);
f=c;
itoa(f,buf,2);
f=strlen(buf);
for(l=8;l>f;l--)//在单字节内对相应位置补0
{
5
strcat(bx,"0");
}
strcat(bx,buf);
}
for(i=0;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是原文件长度
}
2.2依据文件大小,对解压缩后文件和原文件相同性比较进行判断,大小相同则解压缩文
件与原文件相同。
3.主函数设计
主函数主要是为了实现*压缩、解压缩小工具*的菜单栏和调用压缩、解压缩子函数,其中在用户功能选项中有可对用户输入进行容错处理的实现。
对用户输入进行容错处理的编码:
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');
菜单界面设计:
6
图4.菜单界面四、上机调试
以下是我在上机过程中遇到的一些问题以及解决的方案。
1.计算压缩率,就需要设置两个变量,即统计原文件的长度和压缩文件的长度。
统计原文
件的长度好计算,但开始就是不知道在算法里的什么位置添加代码用于计算压缩文件的长
度,后来经过思考和反复上机调试,最终确定应在对哈夫曼编码进行位操作实现压缩存储写
文件过程中,一个一个写字符,变量pt1计数压缩文件的长度。
while(j>=8)
{
for(i=0;i<8;i++)
{
if(buf[i]=='1')c=(c<<1)|1;
elsec=c<<1;
}
fwrite(&c,1,1,ofp);
pt1++;//统计压缩后文件的长度
strcpy(buf,buf+8);
j=strlen(buf);
}
2.文件压缩存储的实现,在建好哈夫曼树对文件字符编过码以后,我们要做的就是如何存
储这些编码,从而实现文件的压缩存储。
我们知道原文件中的字符是以ASCII码形式二进制
“0”“1”按字节存储,例如:
英文字母大A的ASCII码值是65,小b是98,它们在计算机中存储为01000001和01100010,存储两个字符需要两个字节,如果按哈夫曼编码存储,由
于小b可能在原字符型文本文件中出现频率较高,它的哈夫曼编码是110,大A是11101,
小b占3位,大A占5位,这时,两个字符就可以存储在一个字节中,在字符存储位置不变
7
的情况下,节省了存储空间,实现了文件的压缩。
在上机调试的过程中,这一块出现的问题
最多,实现的过程也最难理解,后在查阅相关资料以及老师的指教下,最终达到了预期的目
的。
3.解压缩文件和原文件相似性功能的比较,这里开始我有两个思路,一是逐字符比较解压
缩文件和原文件的相似性,二是统计解压缩文件长度和原文件长度进行比较,若大小完全相
等,则解压缩文件和原文件相同,否则不同。
while
(1)
{
while(strlen(bx)<(unsignedint)p)
{
fread(&c,1,1,ifp);
f=c;
itoa(f,buf,2);
f=strlen(buf);
for(l=8;l>f;l--)
{
strcat(bx,"0");
}
strcat(bx,buf);
}
for(i=0;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");elseprintf("\t解压缩文件与原文件不同!
\n\n");五、用户使用说明
尊敬的用户:
感谢您使用*压缩、解压缩小工具*,本工具为您提供了一个快捷易用的压缩解压缩文本文件的工具,在使用前请仔细阅读以下说明文档,以快速了解本工具的使用方法。
如
果您发现工具存在BUG或者您有任何改进意见请与作者联系,欢迎指教。
进入可执行文件*压缩、解压缩小工具*菜单栏,有三个选项:
8
1.压缩
2.解压缩
0.退出
表1.菜单栏选项在“*请选择相应功能(0-2):
”光标处输入相应数字选择功能,如果超出数字范围,会提示
用户继续输入,直到用户输入功能对应的数字为止。
a.输入数字“1”,选择“压缩”功能,在“请您输入需要压缩的文件:
”光标处输入您要
压缩的文件包括其扩展名(以字符型文本文件为宜),此时注意需要压缩的文件应和该
可执行文件在同一文件夹下,否则提示“文件打开失败!
”。
单击回车,提示用户“请您
输入压缩后的文件名:
”,不需要输入其扩展名。
等待一段时间以后(适文件大小而定),
屏幕提示“压缩文件成功!
”和文件的压缩率或者提示“文件压缩失败!
”。
b.输入数字“2”,选择“解压缩”功能,在“请您输入需要解压缩的文件:
”光标处输入
您要解压缩的文件,不需要输入其扩展名,单击回车,提示用户“请您输入解压缩后的
文件名:
”,需要输入其扩展名。
等待一段时间以后(适文件大小而定),屏幕提示“解
压缩文件成功!
”或者提示“文件解压缩失败!
”。
c.输入数字“0”,选择“退出”,安全推出该工具
六、测试结果
压缩:
图5.压缩文件界面图中bb.hub是压缩aa.txt生成的文件
9
图6.压缩文件生成
原文件aa.txt大小是6.38KB(6538字节)
图7.原文件大小
压缩文件bb.hub大小是3.89KB(3991字节)
10
图8.压缩文件大小
解压缩:
图9.解压缩文件界面
11
图10.解压缩文件生成
aa.txt文件内容
图11.原文件内容
cc.txt文件内容(进行相似性比较可知解压缩文件cc.txt与原文件aa.txt完全相同)
12
图12.解压缩文件内容
七、附录
//哈夫曼编码压缩解压缩程序.cpp2007.6.19
#include
#include
#include
#include
structhead
{
unsignedcharb;//记录字符在数组中的位置
longcount;//字符出现频率(权值)
longparent,lch,rch;//定义哈夫曼树指针变量
charbits[256];//定义存储哈夫曼编码的数组}
header[512],tmp;
/*压缩*/
voidcompress()
{
charfilename[255],outputfile[255],buf[512];
unsignedcharc;
longi,j,m,n,f;
longmin1,pt1,flength,length1,length2;
doublediv;
13
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=(unsignedchar)i;
/*将每个哈夫曼码值及其对应的ASCII码存放在一维数组header[i]中,
且编码表中的下标和ASCII码满足顺序存放关系*/
elseheader[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{
tmp=header[i];
header[i]=header[j];
header[j]=tmp;
14
}
}
}
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{
min1=999999999;//预设的最大权值,即结点出现的最大次数
for(j=0;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
{
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{
f=i;
header[i].bits[0]=0;//根结点编码0
while(header[f].parent!
=-1)
15
{
j=f;
f=header[f].parent;
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{
if(c==header[i].b)break;
}
strcat(buf,header[i].bits);
j=strlen(buf);
c=0;
16
while(j>=8)//对哈夫曼编码位操作进行压缩存储
{
for(i=0;i<8;i++)
{
if(buf[i]=='1')c=(c<<1)|1;
elsec=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;
elsec=c<<1;
}
fwrite(&c,1,1,ofp);
pt1++;
}
fseek(ofp,4,SEEK_SET);
fwrite(&pt1,sizeof(long),1,ofp);
fseek(ofp,pt