哈夫曼编码详解(C语言实现)

解决问题:在信息传输、数据压缩的问题中,我们总希望能够找到一种编码能够将待处理数据压缩得尽可能短。对于这类问题,我们可以采用哈夫曼编码解决。

解决问题的方法:我们可以通过构建哈夫曼树来得到哈夫曼编码。

关于算法我们可以从如下几部分分析:算法的逻辑结构、算法的存储结构、以及算法本身考虑,同时也需要考虑时间和空间的复杂度,关于哈夫曼编码,我们尝试着这几方面分析这个问题

逻辑结构

我们采用的是这种的逻辑结构来解决

原因:因为树的根与左右结点之间的路径都是唯一确定的,所以由根结点到各个叶子结点的路径都是唯一确定的,并且从根结点开始可以到达多个不同的叶子结点,所以我们采用树这种逻辑结构来实现哈夫曼编码。

在创建哈夫曼树的过程中,由于一般编码都是由0和1组成,所以我们创建的哈夫曼树是一颗二叉树,只有左右两个孩子结点。

存储结构

我们采用静态链表的方法来存储哈夫曼树

原因:因为我们会多次删除,插入新的结点,为了方便操作,所以我们采用链式存储的方式存储。但是只要给定了结点的个数,我们可以很容易计算出额外花费的空间,所以我们可以采用静态链表的方式存储,并且我们常常需要为新创建的结点添加孩子结点,为孩子结点添加它的双亲,这样我们必须使用多余的指针来保证这类操作的正确,为了简化理解并且方便我们一般使用静态三叉链表来存储哈夫曼树

静态三叉链表的组成部分

由于使用了二叉树这种逻辑结构,所以我们会有左右孩子域,LChild和RChild,并且为了保证能够清楚直到结点的双亲是谁,我们会有一个Parent域来存储双亲结点。由于哈夫曼编码生成的编码是不等长编码,会根据各个字符的出现频率来生成不同的编码,频率大的编码短,频率小的编码长。所以我们会额外有一个Weight域,权值域来表示各个编码的频率。我们也可以为了直到各个字符的名称也可以加上一个名字域。

所以我们知道了一个链表结点的组成部分有哪些,并且我们可以构建这样一个结构体来存储这些部分

//一个链表结点的组成部分
typedef struct 
{
 //char code; 			//这项属于可选择的部分
 int Weight;
 int Parent;
 int LChild;
 int RChild;
}HuffmanTree[MAXSIZE+1],TreeNode;

算法

哈夫曼树的创建不同于一般树的创建,树的创建一般是由根节点开始,由上往下的顺序创建各个结点。但是哈夫曼树不同,哈夫曼树不同,哈夫曼树是按由下往上的顺序创建(因为我们不清楚根节点的左右两个结点是是什么,我们只直到哈夫曼树的叶子结点)

由于这个原因,在哈夫曼树的创建过程中,我们我们都是将编码最长的两个结点作为新子树的左右孩子(因为编码最长代表着一定是在树的最下边位置),由于编码的长度代表着字符的权值(频率)

因此在操作中,我们先得到最小还有次小两个权值的结点,记为mMin和sMin。用这两个结点创建一个新的结点(构建了一颗子树)。

之后就将这个结点添加到已有的结点中,再将之前的最小,次小结点从结点中删除

之后就是重复上面的操作

在已有的结点中(包括之前添加的新结点,但上次的最小值,次小值因为删除了,所以不在已有结点中)找到最小,次小两个结点,在组成一颗新的子树,添加到已有结点中,然后再将之前最小、次小结点从结点中删除…

这样一直操作下去,直到最后只剩下一个结点(N个结点,进行N-1次操作,因为最后一个结点没办法找到另外一个结点合并,N的结点只会创建N-1个新树,即进行N-1次操作)

这样一颗哈夫曼树就创建完毕了

//哈夫曼树的创建
void CreatHuffmanTree(TreeNode *ht)  //哈夫曼树的创建 
{
 int MNum,SNum;
 InitHuffmanTree(ht);		//哈夫曼树的初始化
 InputWeight(ht);		//得到字符的数目和每个字符的权值(频率)
 for(int i=1;i

注意:

上述的代码并没有删除最小,次小的结点的部分,因为数组的删除不太方便,所以由于最小,次小的结点的双亲已经被修改了,我们将查找最小,次小值的判断条件添加一个条件—结点的双亲域不为0(初始化时,所有结点的双亲,孩子域都为0),若不为0,则代表该结点已经被删除了

同样我们并没有将新结点添加到已有结点中,由于我们定义了一个len(全局变量,代表数组的长度),我们将得到最小,次小操作的范围规定为1到len(个人习惯,数组下标从1开始),len一开始等于输入的结点数NodeNum,随着新结点的创建,len的值也会增加,所以我们通过len表示添加新结点这个操作

哈夫曼编码

尽管得到了哈夫曼树,但是我们并没有得到各个字符的哈夫曼编码,所以我们需要由哈夫曼树得到哈夫曼编码

一般默认左0右1,我们需要注意一点,左0带的是走左孩子这条路径,同理右1代表走右孩子这条路径,即编码的0和1是指路径而不是结点,这点不要弄错了

我们先从根开始遍历一次哈夫曼树,先找到哈夫曼树的根,由于新添加的结点是在len位置,所以最后添加的结点也是len位置(数组下标从1开始,len=2*N-1,N为字符数目),找到根节点后,按照树的遍历,从左结点开始,由于走左的那条路径,我们需要存储一个0,所以我们需要一个存储哈夫曼编码的空间,我们使用什么存储呢?

我们可以使用一个来存储哈夫曼编码,为什么不用队列呢?

因为我们如果一个结点的左结点遍历完,返回到结点本身之前会将代表该路径的0出栈(为什么在返回之前出栈后面会解释的),在遍历右结点的时候,再入栈一个1就可以完成这个操作

但是当我们如何判断当前结点是我们需要编码的字符结点呢?由于我们需要编码的字符都相当于哈夫曼树的根节点,所以我们可以根据当前结点是否有左孩子或者有右孩子(即左孩子域或者右孩子域是否为0)

如果在结构体内定义了一个编码名称,则可以判断当前字符是否有名称(初始化可以把名称一起定义为0,输入的时候再改)

由于一个子树结点必定是由两个结点组成,所以不可能出现有一个结点其中一个域为空,另一个域不为空这种情形,所以再判断的过程中只需要判断结点的左孩子域是否为空或者右孩子域是否为空其中一种即可

当我们知道了如何判断一个结点是否是编码字符之后,我们应该如何输出编码字符的哈夫曼编码呢?由于我们是用栈来存储的编码,所以我们只按顺序输出栈内的值(只需要输出,而不是出栈,若是出栈的话,其余结点的编码会被破坏)

输出之后要在返回之前执行一次出栈操作,表示返回上一个结点,也表示这条路径已经结束

0代表左路径,1代表右路径,若不出栈,当走完左路径的所有编码结点,要返回上一个结点,去访问右路径的所有编码结点的时候,返回上一个结点后,但是上一个结点的左路径还在栈中,这会导致右路径的编码全部出错。

void PrintHuffmanTree(TreeNode *ht,int root)  //输出哈夫曼编码
{
 int data;
 if(ht[root].code!=0)   //由于我的结构体带有名称,所以我使用了编码名判断,判断当前结点是否是编码的结点,若不是,由于初始化的code编码名为0,所以由此判断 
 {
  printf("%c:",ht[root].code);  //输出编码名 
  PrintStack();  //输出编码 
  PushStack(); //每一次返回上一步,即当前结点已经有了编码,所以将栈顶出栈,计算其余的编码 
  return ;  
 }
 PopStack(0);  //若计算左结点则入栈0.右结点要入栈1
 PrintHuffmanTree(ht,ht[root].LChild); 
 PopStack(1); 	//走完左就走右,右入栈1
 PrintHuffmanTree(ht,ht[root].RChild);
 PushStack();  //返回前出栈,代表当前路径结束,要开始其他路径的编码计算
 return ;
 }
 void PrintStack(void)  //输出编码,若需要翻译则需要用另外一个数组存储编码名和编码然后再进行翻译 
{
 for(int i=0;i

给一串编码,要求输出编译后的结果

我们知道哈夫曼树的每个叶子结点都代表了一个编码结点,所以我们只需要按照遇0走左,遇1走右,从根节点开始走一直走到叶子结点,再输出叶子节点的编码名并重新从根节点开始就可以了

void Translate(TreeNode *ht)
{
 char ch[80];
 int i=0,j=len;
 printf("请输入要进行编译的密码:");
 gets(ch);
 while(ch[i])
 {
  if(ch[i]=='0')   //先执行再判断,因为哈夫曼树第一个结点肯定不是编码结点,所以需要先做处理再进行判断是否是编码结点 
  {
   j=ht[j].LChild;  //若为0,走左分支 
  }
  if(ch[i]=='1')  //若为1,走右分支 
  { 
   j=ht[j].RChild;
  }
 
  if(ht[j].code!=0)   //判断当前哈夫曼树结点是否是编码结点,若是编码结点,则其code值为编码 
  {
   printf("%c",ht[j].code);
   j=len;  //将结点重新放到哈夫曼树的根节点位置,重新开始,直到所有密码编译完成 
  } 
  i++;  //i自增
 }
}

你可能感兴趣的:(算法总结)