提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
基本概念与我的一些想法
提示:这里可以添加本文要记录的大概内容:
路径长度:按我的理解简单粗暴来说就是同一棵树两结点间要走过的路
树的路径长度:相当于上面那个概念的一般情形,即树的根结点到树上结点要走的路径长度
权重:不妨理解成树上果实的重量(标准的概念解释是:赋予树中某结点一个有意义的值)
结点的带权路径长度:是指该树的根结点到该结点路径长度和该结点权重的乘积
ps:树的每一个结点都拥有自己的权重,而所有叶子结点的带权路径长度之和被称为树的带权路径长度。并且哈夫曼树并不唯一,但是带权路径长度一定是相同的(原因在于这个过程并没有对左右子树进行限定,树的高度并不是唯一的)
哈夫曼树是在结点和路径确定情况下,带权路径长度最小的一种最优二叉树
哈夫曼编码则是对应的最优的算法编码(这时将权重理解成频率)
更多详细的概念可以参考:
https://baike.baidu.com/item/%E5%93%88%E5%A4%AB%E6%9B%BC%E6%A0%91/2305769?fr=aladdin
我的想法是:首先是证明这个最优解,我们寻求一种最优解,假定一串字符中ni元素出现次数为fi;假设ni的编码长度为li,则解压该段字符总长为sigma(fili) 联想到数学中的排序不等式
Rearrangement Inequality:
a1<=a2<=…an b1<=b2<=…bn (1)
the inequalities is:
a1b1+…anbn>=a1bn+…anb1 (2)
由此看出,我们应该让权值小的叶子远离根节点,权值大的叶子结点靠近根节点
首先,我们需要构建森林,也就是将每一个叶子结点当成一颗树,同时构造出一个辅助队列按照权值大小存储结点(这或许需要用到数据结构中队列一,相关的知识)其次我们需要对权值(也就是频率较小的两个结点)进行合并,成为新的一个结点(该结点权值为两结点之和)这样做完后,我们再在刚刚构造的队列中删除这两个小权值结点,替换成新的权值更大的结点重复这样的操作…直到最终我们会发现,队列中只剩下一个数,这个数就是先前提到的树的带权路径长度,而之前的整片森林也成了一颗树,这棵树就是哈夫曼树。然后在树构建完成的基础上,我们人为规定向左为0向右为1,便可以依次得出每个结点的哈夫曼编码。主函数程序流程:输入一系列字符–>判断字符出现的频率(也就是权值)–>
输出字符和它的频率数–>据此构造哈夫曼树–>求出哈夫曼编码–>输出哈夫曼编码
提示:以下是本篇文章正文内容,下面案例可供参考
算法实现
树的构建 相比于一般树的创建,哈夫曼的树的创建要更为特殊,一般树创建是从根节点开始,然后依次从上到下构建其他结点。但是对于哈夫曼树,因为根本不清楚对于一个根节点来说,它的左右结点是什么,所以这个时候我们需要反过来,也就是从下往上构建(逐个依次选出较小的两个结点);依据这种想法结合我在4.1中提出构建森林的观点,我们可以写出构造树的伪代码:(方便理解这里用中文)
for(进行结点数次循环){
选择出次小,较小;
结点数目++;
新权值等于左右子权值和;
用数组存左子值;
用数组存右子值;
将左子双亲修改成新子树数组下标;
将右子双亲修改成新子树数组下标;
}
上面的伪代码用于构建森林,但是注意到其实这串伪代码没有对结点进行删除,由于本人在数据结构和算法方面知识欠缺,还不知如何对数组进行删除,我的想法是,干脆在初始化时,把所有结点都赋成一个值(这个值在后续不可能出现),然后在后面如果结点已经不等于这个特定值,那么很显然,就表示这个结点已经被我删除。这是树的构建
编码的构建 对于编码的构建,这个部分我认为是最难的,我查找了一些相关数据结构和算法的资料(栈和队列),我发现我们需要先从树根开始对树进行遍历,找到根节点后向左走存储0,向右走存储1,对于存储空间的选择在我查阅完资料后发现还是有讲究的
我们到底是使用一个栈来存储哈夫曼编码,还是使用一个队列呢?
在我参考学习的一篇文章中认为:由于在将左结点遍历完返回结点本身前会将代表路径的0出栈,所有遍历完左后再遍历右结点时让1入栈就可以很好完成这个操作。并且我们1可以根据当前结点是否有左右子来判断当前结点是不是需要编码的结点
根据以上想法,同样可以写出编码构建部分的伪代码(方便起见还是用中文):
for循环判断i结点双亲结点
{if左孩子等于i—>左子树编为0;
else—>右子树编为1;
移动结点;}
将i+1个字符编码放入编码对应数组;
//ps:此伪代码,方便本人在编程时看懂,所以大多采用方便自己看的懂的形式,望读者海涵,具体代码见附件。
代码实现``
#include <stdio.h>
#define MAXQUANZHI 520 //利用宏定义一个对输入权值做最大限制,可以改
typedef struct{
int quanzhi;
int Fatherjd,ljd,rjd ; //分别表示父结点,左结点,右结点
}HfmNode; //定义一个结点的结构体
typedef struct{
int a[8];
int u;
}HfmCode; //定义一个编码的结构体
HfmNode HNode[100];
HfmCode HCode[100]; //100是一个相对大的数 以确保后面数组不会越界
int nodesum; //表示结点个数
void HuffmanTree(void);
void HuffmanCode(void);
void ShuchuHuffcode(void);
void HuffmanTree(void){
int i,j,k,l,m,n;
scanf("%d",&nodesum);
for(i=1;i<2*nodesum;i++) //关于这里为什么是2*nodesum 可以由特殊到一般的想法,由递推的思想给出关系式
{
HNode[i].quanzhi=0; //对所有节点初始化为-1 从1开始赋值,方便后面操作
HNode[i].Fatherjd=-1; //初始结点全部定义成负值,无所谓负多少,因为结点不可能是一个负值
HNode[i].rjd=-1;
HNode[i].ljd=-1;
}
printf("请输入这些节点的频率(即权值)共%d个\n",nodesum);
for(i=1;i<=nodesum;i++)
scanf("%d",&HNode[i].quanzhi); // 构建循环输入权值
for(i=1;i<nodesum;i++) // 寻找较小的两个结点 开始构造出哈夫曼树
{
k=MAXQUANZHI; //k l两个数表示最小和次小
l=MAXQUANZHI;
m=0; //用于记录下标 ,所以每次进循环需要初始化
n=0;
for(j=1;j<nodesum+i;j++) //选出最小和次小的权值
{if(HNode[j].Fatherjd==-1&&HNode[j].quanzhi<k) //第一个条件作用是表示叶子结点没有双亲结点后面一个条件限制在最大权值范围
{l=k;
n=m;
k=HNode[j].quanzhi;
m=j;
}
else
if(HNode[j].Fatherjd==-1&&HNode[j].quanzhi<l)
{l=HNode[j].quanzhi;
n=j;
}
}
HNode[m].Fatherjd=nodesum+i; //合并两个树叶,让他们有同一个双亲结点
HNode[n].Fatherjd=nodesum+i;
HNode[nodesum+i].quanzhi=HNode[m].quanzhi+HNode[n].quanzhi;
HNode[nodesum+i].ljd=m;
HNode[nodesum+i].rjd=n;
}
}
void HuffmanCode(void){
HfmCode w; //构造暂存的缓冲变量
int i,j,c,b;
for(i=1;i<=nodesum;i++){
w.u=nodesum;
c=i; //此变量用于后面依次遍历结点,从叶子结点向上
b=HNode[c].Fatherjd; //b是c的双亲
while(b!=-1){ //对双亲结点进行判断
if(HNode[b].ljd==c)
w.a[w.u]=0; //全局数组嵌套调用 规定向左0 向右1
else
w.a[w.u]=1;
w.u--;
c=b; //移动
b=HNode[c].Fatherjd;
}
for(j=w.u+1;j<=nodesum;j++)
HCode[i].a[j]=w.a[j];
HCode[i].u=w.u;
}
}
void ShuchuHuffcode(void){
int i,j;
printf("每个叶子结点的哈夫曼编码分别为\n");
for(i=1;i<=nodesum;i++)
{
for(j=HCode[i].u+1;j<=nodesum;j++)
printf("%d",HCode[i].a[j]);
putchar('\n');
}
}
int main(void){
printf("输入节点个数\n");
HuffmanTree();
HuffmanCode();
ShuchuHuffcode();
return 0;
}