哈弗曼编码C实现二叉树的图形化输出已经界面.docx
《哈弗曼编码C实现二叉树的图形化输出已经界面.docx》由会员分享,可在线阅读,更多相关《哈弗曼编码C实现二叉树的图形化输出已经界面.docx(20页珍藏版)》请在冰豆网上搜索。
![哈弗曼编码C实现二叉树的图形化输出已经界面.docx](https://file1.bdocx.com/fileroot1/2022-11/27/7a4dd848-816b-4ae4-93f5-bdf2f94a6545/7a4dd848-816b-4ae4-93f5-bdf2f94a65451.gif)
哈弗曼编码C实现二叉树的图形化输出已经界面
软件工程学院
程序设计实践(下)实验报告
题目:
哈夫曼编码/译码器
姓名:
张祺
学号:
10109345
班级:
软件工程3班
一、问题解析(对问题的分析、解题思路与解题方法)
问题主要分为三部分,分别是文件的操作、界面的设计、文件的压缩,其中界面的设计和文件的压缩又是基于哈弗曼编码的,因此,构造哈弗曼树和获取哈弗曼编码是程序的关键。
1、文件的操作:
程序中文件的操作主要是文件的读和写,关于这点,基本上没有遇到什么问题。
2、界面设计
前面已经提到,界面设计是基于哈弗曼树和哈弗曼树的小时,我分到了这一块,下面做介绍。
由于哈弗曼树是正则二叉树,即对于每个结点,要么其左右孩子都存在,要么都不存在。
但对于树形结构来说,其变化多端,每一行的结点数不确定,虽然第二层(第一层只有根结点)和最后一层结点数没有任何确定的关系,但他们之间却有相互制约关系,即底层结点的个数关系着上层(尤其是第二、三层)结点之间的距离,这也是最难把握的地方。
最初,我用矩阵保存同一层结点之间的关系,但后来发现,兄弟结点的关系容易获得,但如果两结点不是兄弟结点而是堂兄结点时,这种关系就比较难找了,再不理想一点,如果两结点在的根结点父节点来自不同的结点,这种关系就更难确定了。
然后,我又想想到了从根结点出发仿照先序遍历的原理逐个打印结点的信息。
很快,我发现,这种方法也是不行的,原因在于在先序遍历时可也借助栈或者递归栈来储存后面要访问的元素。
但是想要找到这个元素的具体位置比较难,所以这种方法也很快被否认。
一直找不到可行的办法,想过用打表,也想过放弃。
但是实在不想,一个原因是这是我的任务,如果我放弃或者做的不够完美,这将会影响我们组所有同学的成绩。
另一个原因就是我一直认为,只要是能想的到的,就能用代码实现,这也是编程的精髓。
无奈之下,我开始查找资料,突然,一张满二叉树的图片(其实是一张家谱图)给了我灵感。
我为什么要根据结点选位置呢?
完全可也根据位置确定这点的结点啊……所以我的最初构想是先打印出一个树形结构,然后根据每个结点的信息,将其输出到指定的位置,最后我就是用这种方法做到的。
3、构造哈弗曼树、获取哈弗曼编码
构造哈弗曼树的关键在于每次将权值最小的两个结点合并成一个新结点,如此循环,直到只剩一个结点。
在获取编码时,从每一个叶子结点出发,访问哈弗曼树的根结点,所走过的路径刚好和编码相反。
二、任务分工及进度计划
我们组的成员还有王倩、求春磊、章力挺。
最初分工时,王倩负责文件的读写操作;求春磊负责文件的编码压缩这一块;章力挺负责构造哈弗曼树、获取哈弗曼编码这一块;我负责疏导图形化输出、界面这一块。
总体来说,小组的进度比较慢,原因在于第一个程序还没写好。
而且我的任务在中间,只有有了哈弗曼编码才能进行树形结构的输出,所以在开始时,前面的问题我也解决了。
下面谈谈我的见解。
文件的读写基本上没有什么大的问题,在统计字符频率时,我统计了128个常见字符,用了点小算法,效率比较高。
在构造哈弗曼树时出现了瓶颈。
究其原因,主要是以前没有接触过哈弗曼树,只有老师在上课时提到过,所以,面对一个新问题,首先需要时间去熟悉。
等到哈弗曼树构造完成,获取编码也就相应比较简单了
三、数据结构选择、算法设计
前面已经提到,这个程序我基本上全都做了,现在就出现的问题分析。
1、统计字符频率
常见的字符有128个,如果检索一个一个判断,那样的代价会很大的。
由于128个字符分别对应1~~128的ASCII码值,所以可以定义一个128单元的数组,用字符代替下标。
这样一来,对于每一个字符,其计算频率其出现一次,下标为该字符的元素递增1。
这样可以高效的简单的统计每一个字符出现的频率。
2、构造哈弗曼树、获取哈弗曼编码
在问题分析中叶提到,构造哈弗曼树的关键是每次将两个权值最小的结点合并成一个新的结点。
教材上采用0号元素不用,通过判断结点的父结点是否为0确定该结点是否要被计算。
在我的计算中,我用0号结点,而且另外开辟了一个存放结点权值的副本数组,这样一来,每次都是对结点判断,被合并的结点权值变为-1,最后用副本还原所有结点的权值。
结点的结构体定义如下:
structHafuman
{
intweight;//权值3
intparent;//父结点的位置
intleft_child;//左孩子的位置
intright_child;//右孩子的位置
};
在构造哈弗曼树时,所有结点(包括本来有权值的结点和通过合并的到新的权值的结点),其地位是等价的,因此,最好把两种结点放在一起,这样方便访问。
所以在有128种字符的前提下,开辟128*2个结点,前128个结点存放本来就有权值的结点,后127个结点存放新合成的结点,这样一来,访问两种结点就只需要一个循环。
由于128个结点不是很大,所以用线性结构的数组存储。
在获取哈弗曼编码时,由于是从前128个结点中有权值的结点开始访问根的,因此访问得到的编码正好和要得到的哈弗曼编码相反,估只需逆转就可得到哈弗曼编码。
3、图形化输出树形结构
前面提到,在输出树形图时采用有点类似模板的方法输出树,即先打印全部树枝,再根据情况条件树叶,最后擦掉多余的树叶。
但是,这种方法任然有缺陷。
根据二叉树的性质,第k层的结点个数最多为2k-1,当是第7层时,需要结点26即64个结点。
尽管对界面的宽度做了扩展,但在一列打印64个结点仍然很难做到,或者即使做到,效果也不一定理想。
所以最后树的深度在6以内,即对于深度大于6的树,只输出前6层。
在打印树形结构时,将每一层第一个结点所在的坐标记录下来,在打印结点时首先找到这个坐标,这样一来,每层头结点坐标相当于提供了一个借口。
由于已经打印出了满二叉树的树枝,这样一来当这个位置没有结点时,最好把它坐在的树枝擦掉,以免影响美观。
擦除和打印的类似,不同的是擦除只是打印空格。
为了界面美观,我将树枝的颜色和结点的分成了两种,定义了color(intn)函数这个函数的功能在于调用这个函数的时候光标打印出的字符全部变为指定的颜色。
4、编程与程序清单
前面已经提到,这个程序的大部分问题我自己都解决了,现做一下说明。
1、统计字符频率
在定义好结点的结构后首先声明一个2*N大小的数组,为了所有函数调用的方便,把这个数组声明为全局变量。
首先是打开文件
if((fp=fopen("C:
\\hafuman.txt","r"))==NULL)
{
printf("cannotopenthefile!
\n");
exit(0);
}
等打开文件后,通过一次循环就可以统计所有字符的频率
c=fgetc(fp);
while(c!
=EOF)
{
a[c]++;//a[N]是结点权值的副本
node[c].weight++;//结点权值递增
length++;//统计文件长度的变量递增
c=fgetc(fp);//获取下一个字符
}
就这样,只需要循环一次就可以得到所有结点的权值
在打印统计结果时,我用了表格。
由于有些特殊字符,比如:
换行("\r")、回车("\n")、空格("")、tab("\t")等键无法打印,或者即使打印出来也看不见,所以,对于特殊字符,我输出了他们的中文名称。
由于有128个字符,如果使用同一种颜色打印,未免太单调,而且不易区分,所以,我有分别设定了表格、字符、出现次数的颜色,把它们区分开来。
最后的效果如下:
就这样,完全打印输出了128个字符出现的频率。
在这里特别声明,由于默认的窗口太小,我用了拓展窗口的函数,
system("modeconcols=130lines=38");
这即表示窗口是130列,38行。
同样方法,在打印哈弗曼编码时也用该方法,效果如下:
2、构造哈弗曼树、获取哈弗曼编码
由于前面已经统计了每个字符出现的频率,这些频率就是字符的权值。
构造哈弗曼树的关键点是每次将权值最小的两个结点合并成一个结点,而且保证合并过的结点此后不会再被统计。
我设计是将没出现的字符频率即权值初始为-1,出现的,其权值就是1、2、3、4.。
。
。
这样避开了0的特殊性,由于在统计字符时,统计的是最小的,而0也可以算是最小的,这样会带来不便。
所以,直接用正、负两种状态区别。
具体代码如下:
voidGreate_Hafu()
{
intmin_1,min_2;
intLeft,Right;
inti;
for(i=N;i<2*N;i++)
{
Find_Min(min_1,min_2,Left,Right);
/*找到两个权值最小的结点,最小权值为min_1、min_2,最小权值所在位置为Left、Right*/
if(Right!
=-1)//有最小两结点
{
node[i].weight=min_1+min_2;//新结点
a[i]=node[i].weight;//a[N]与node.weight[N]同步
a[Left]=node[Left].weight;
a[Right]=node[Right].weight;
node[i].left_child=Left;
node[i].right_child=Right;
/*新增了结点,该结点是刚最小权值两结点的父结点*/
node[Right].parent=i;
node[Left].parent=i;
//当前最小两结点的根结点为刚产生的新结点
node[Right].weight=node[Left].weight=-1;
//当前权值最小的两结点权值清0,以便后面的选择
}
}
for(i=0;i<2*N;i++)
node[i].weight=a[i];
//最后对所以结点的权值还原
}
由于前N个结点存放都是出现字符的权值,所以在合并新结点时,新结点的位置一定在N~~2*N直接。
所以在访问1~~N之间的元素时,循环用的是for(i=N;i<2*N;i++),这样当找到两个权值最小的结点时,将其权值赋值给一个新的结点,而这个新结点一定在N~~2*N之间。
找到的权值最小的两个结点其父结点都指向了新的结点,为新结点的左、右孩子也分别指向了两个权值最小的结点。
这一年以来,相当于建立了一个双向二叉树,即通过父结点可以直接找到其左、右孩子结点,也可以通过左、右孩子结点直接找到其父结点,这么做的原因主要是为了后面获取哈弗曼编码的方便。
由于已经建立好了哈弗曼树,那么,获取哈弗曼编码的过程就是从叶子结点出发,访问根结点的过程。
具体代码如下:
voidGet_Code()
{
charstr[2*N];//声明一个临时存储编码的数组
for(inti=0;i<2*N;i++)
{
if(node[i].weight>0)//权值为正时才可能有父结点
//只有权值不为-1的结点才有其父结点
{
intp=i,k=0,j=0;
memset(str,0,sizeof(str));//临时数组全部置0
/*找到了一个有权值的结点时,
需要从此结点出发,访问根结点*/
while(node[p].weight>0)
{
intparent=node[p].parent;
if(node[node[p].parent].left_child==p)
//判断是当前结点是左孩子
str[k++]='0';//左为0
elseif(node[parent].right_child==p)
str[k++]='1';//右为1
p=parent;
parent=node[parent].parent;
//父结点继续访问根
}//这个循环相当于是递归的过程
while(k--)
hafucode[i][j++]=str[k];
//最后编码赋值,(是逆序编码)
}
}
}
由于0~~2*N之间的每一个结点都可能存有有权值的结点,所以循环是从0~~2*N。
由于前N个结点存放的是本来就有结点的权值,也就是说,前N个结点存放的恰恰是叶子结点,其所在的位置也应该是树的最下层,其父结点一定在N~~2*N之间。
但是,由于N~~2*N之间的结点可能还有父结点,因此在N~~2*N之间也要循环。
最后到01字符串是从下往上的编码,而真正的哈弗曼编码又是从上到下的,刚好相反,所以需要逆转。
3、图形化输出
前面已经说了,我图形化输出时,先打印了树形结构,在打印各个结点。
在打印树枝时,我用了暴力的方法,即一层一层输出,并没有用规律循环。
但是,规律肯定是有的,只是我的深度只有6,找规律还不如直接打印来的快。
最初打印的树枝图如下所示:
这样一来,在打印结点时只需找到其所在层的入口。
intx=1,y=END-10;
local[5][0]=x;
local[5][1]=y;
gotoxy(x,y-1);
for(i=0;i<16;i++)
printf("╱╲");
x+=3,y-=2;
local[4][0]=x;
local[4][1]=y;
gotoxy(x+1,y-1);
for(i=0;i<8;i++)
printf("╱\t╲\t");
其中local[5][5]是声明的每一层入口坐标的起始坐标的全局变量
上述代码实现了第6、5层树枝的打印。
上面所示图形是左右完全对称的,但是,在打印结点时,由于仅仅知道每层的入口地址,而不知道某个结点的具体位置。
因此在打印结点时需慢慢尝试。
for(i=0;i<2*N;i++)//第一个
{
if(node[p].left_child==i)
{
Print_Data(local[3][0],local[3][1],i);
for(i_1=0;i_1<2*N;i_1++)
if(node[i].left_child==i_1)
{
Print_Data(local[4][0],local[4][1],i_1);
break;
}
for(i_1=0;i_1<2*N;i_1++)
if(node[i].right_child==i_1)
{
Print_Data(local[4][0]+8,local[4][1],i_1);
break;
}
for(i_1=0;i_1<2*N;i_1++)
if(node[node[i].left_child].left_child==i_1)
{
Print_Data(local[5][0],local[5][1],i_1);
break;
}
for(i_1=0;i_1<2*N;i_1++)
if(node[node[i].left_child].right_child==i_1)
{
Print_Data(local[5][0]+6,local[5][1],i_1);
break;
}
for(i_1=0;i_1<2*N;i_1++)
if(node[node[i].right_child].left_child==i_1)
{
Print_Data(local[5][0]+8,local[5][1],i_1);
break;
}
for(i_1=0;i_1<2*N;i_1++)
if(node[node[i].right_child].right_child==i_1)
{
Print_Data(local[5][0]+14,local[5][1],i_1);
break;
}
break;
}
}
上述代码就是最熟知左下角三层的输出,其中Print_Data(intx,inty,inti)函数表示将在0~~2*N中的第i个结点输出到坐标为(x,y)的位置上。
依次重复,输出所有结点,最后会把树图形化输出。
当输出所有结点时,有些没结点的树枝还在,如果不擦除,影响美观程度,所有还要对没结点的树枝擦除,如下:
为了擦除多余的树枝,在打印结点时,判断该结点是否有权值,如果有,则正常打印,如果没有,则擦除该结点的左右树枝,具体代码如下:
voidPrint_Data(intx,inty,inti)
{
intk;
gotoxy(x,y);
if(node[i].weight>0)
{
if(i{
if(i==10)printf("换");
if(i==32)printf("空");//特殊字符打印中文名称
elseprintf("%c",i);
//如果是出现的字符,为了直观,打印字符
}
else
printf("%d",node[i].weight);
//是新合并的结点时只需打印权值即可
}
else
{
//通过判断node[node[i].child].weight来擦除
if(y==local[5][1])//第6层的擦除
{
intk;
for(k=0;k<130;k++)
if(1+k*8==x)
{
gotoxy(x+1,y-1);
printf("");
break;
}
for(k=0;k<130;k++)
if(7+k*8==x)
{
gotoxy(x-1,y-1);
printf("");
break;
}
}
if(y==local[4][1])//第5层的擦除
{
gotoxy(x+1,y-1);
printf("");
gotoxy(x+7,y-1);
printf("");
}
if(y==local[3][1])//第4层的擦除
{
for(k=0;k<3;k++)
{
gotoxy(x+k+1,y-1);
printf("");
x++,y--;
}
y+=3,x-=3;
for(k=0;k<3;k++)
{
gotoxy(x+14-k,y-1);
printf("");
x--,y--;
}
}
if(y==local[2][1])//第3层的擦除
{
for(k=0;k<7;k++)
{
gotoxy(x+k+1,y-1);
printf("");
x++,y--;
}
x-=7,y+=7;
for(k=0;k<7;k++)
{
gotoxy(x-k+31,y-1);
printf("");
x--,y--;
}
}
}
}
上述代码中,凡出现puts("")的都表示擦除没有结点的边
我只输出6层的原因如下:
当深度是6时,有32个叶子,32个叶子可以表示大部分常见的字符。
另外如果输出7层,则需要64也叶子,这对不经对屏幕长、宽都有要求,而且观看起来也不是很方便,不够直观,因为当有7层时,前面几层的兄弟结点间的距离会很大的。
至此,树形结构完全输出,最后效果如下:
其中,数字一般表示该结点还有左、右孩子,只是由于屏幕限制,没被打印。
3、文件的输出界面
由于我负责界面这一块,所以在最后的文件操作时我也参与了稳定输出。
输出对比文件时,为了更好的对比,我的界面如下:
如果文字过长,会显示“文字过长”未予显示”,最下面还有选择按钮。
在逐个打印字符时,如果当前字符是换行时,如果只是简单的打印,会引起后面的布局混乱,所以在打印字符时还应该做判断
p=fopen("C:
\\hafuman.txt","r");
gotoxy(x,y);//起始位置
intcount=0;//字符计数器
charc;
color(6);
c=fgetc(p);
while(c!
=EOF)
{
if(c==10)//如果是换行符,光标换两行
{
y+=2;
count=0;
gotoxy(x,y);
}
else
printf("%c",c);
count++;
c=fgetc(p);
if(count&&count%52==0)//没行显示52个字符
{
y+=2;//换行
gotoxy(x,y);
}
if(y>34)//超出行数
{
puts("\t\t文字过长,部分未予显示");
break;
}
}
5、测试方法、测试数据与测试结果
起始界面如下
其中,《对文件操作》又包含几个子选项
测试数据主要从文件中读取,加入读取字符串"HANGZHOUDIANZIKEJIDAXUE"
字符频率显示如下:
树形结果图如下:
字符编码如下:
编码文件如下:
文件对比显示如下:
5、总结
通过这个程序,我学会了怎么面对一个陌生问题。
首先是构造哈弗曼树,虽然方法教材上有所介绍,但那毕竟是教材,虽然经典、简介,但毕竟不是自己掌握的,我们要做的就是怎样把教材的大小化为自己的知识,教材只是起指导作用。
比如,“每次将权值最小的两个结点合并成一个新结点”,这才是教材教给我们的,至于怎么实现,我想每个人都有自己的方法,只是简易程度。
图形化输出树形结构,这个问题从没遇到过,而且也没思路,一味的盲干是不会其任何效果的。
只有现确定方法(如我采用的是现打印慢二叉树的树枝,再添加叶子),问题的关键点就是确定这个方法。
至于用代码实现,一般来说不会有什么问题。
这个程序是以团队形式完成的,在小组中,组员之间存在水平差异,有时候自己很急,但组员不尽力,这给自己的继续带来了不便。
可能是由于这是个小程序,组员之间的协助体现的不是很具体。
但在以后的生活中,我们必须要有团队精神,特别是对于我们专业的同学。
问题的全部解决归功于不断的思考。
还是那句话,我坚信只要是能想到的,就能用代码实现。
热门网http:
//www.remen.org32T4DAzl8z9L