哈夫曼树可以说是最经典的一类二叉树了,它出现的频率很高,也有广泛的应用。
哈夫曼树的建立虽然不算太难,但笔者在初次接触并实现对应的操作时却也遇到了一些困难,所以在这就分享一些个人的经验与见解,希望对初学者刚刚接触哈夫曼树能起到一定的帮助。
关于哈夫曼树的动态生成的原理,很多优秀的博客都有详细的讲解,下面推荐一篇非常好的博客,用大量的图片gif素材可视化地展现了整个过程,大家可以参考着上手理解。
(29条消息) 哈夫曼树(C语言实现)_2021dragon的博客-CSDN博客_哈夫曼树c语言
其实哈夫曼树的原理并不难理解,相信大家通过这个博客能很快上手,我们就从具体代码的角度进行分析。
其实建哈夫曼树的步骤并不复杂,虽然它的单个结点定义量可能比较多(权值,字符值,父节点,左右孩子结点),但是核心只有一个:每次选择权值较小的两个结点值,合并生成新树(新的结点),并且动态更新结点的父节点与孩子节点的值。
在此之前,我们只需要用一个数组存储所有结点的权值(在这里面的字符值其实是可有可无的),并且对未建树前的结点进行简单的初始化(parent,ichlid,rchild赋值0)。在动态更新后续产生的新结点时,会对选择的结点进行parent的重新更新。
下面是建立哈夫曼树的核心代码。
void Create_HuffmanTree(HuffmanTree &HT,int n,int *w)
{
int m,s1,s2;
m=2*n-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
for(int j=0;j<=m;j++)
{
HT[j].parent=HT[j].lchild=HT[j].rchild=0;
}
for(int j=1;j<=n;j++)
{
HT[j].weight=w[j-1];
}
for(int i=n+1;i<=m;i++)
{
Select(HT,i-1,s1,s2);
HT[s1].parent=i;
HT[s2].parent=i;
HT[i].lchild=s1;
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight;
}
}
注意,我们用了动态分配空间,对于n个结点的树,最终产生的哈夫曼树会有2n-1个结点(进行n-1次合并,每次合并会减少一棵子树),而我们为了方便,我们HT是从下标为1开始存储值的。这个时候我们需要注意分配空间时要分配m+1个HTNode(我曾经因为这个疏忽,耗费了大量时间进行测试,但是一直出现运行的错误但却不知道原因)。
这里面还有一个重要的函数Select,它的作用就是挑出原结点里权值最小的两个(并且之前没有被挑中过的),然后返回它们的下标s1,s2。
这个函数的写法可以有很多,本质就是一个排序函数,只是我们只需要得到值最小的两个。
我们可以利用排序使之有序化,但是这样太浪费了。所以,我选择的是简单的扫描比对找出两个最小的。
Select函数如下。
void Select(HuffmanTree HT,int i,int &s1,int &s2)
{
int min1,min2;
for(int j=1;j<=i;j++)
{
if(HT[j].parent==0)
{
min1=j;
break;
}
}
for(int j=min1+1;j<=i;j++)
{
if(HT[j].parent==0)
{
if(HT[j].weight<HT[min1].weight)
min1=j;
}
}
for(int k=1;k<=i;k++)
{
if(HT[k].parent==0 && k!=min1)
{
min2=k;
break;
}
}
for(int k=min2+1;k<=i;k++)
{
if(HT[k].parent==0)
{
if(HT[k].weight<HT[min2].weight && k!=min1)
min2=k;
}
}
s1=min1;
s2=min2;
}
这就是这部分的核心代码。
但是哈夫曼树的建立,往往就会与哈夫曼编码紧密关联。所以,在这里笔者也给出对应的哈夫曼编码的代码分析。
哈夫曼编码的原理同样可以参考上面的博客,本质就是在哈夫曼树上,以根节点出发,向下延伸到某一结点的过程中,对之路径上进行对应的编码。我们一般左子树为0,右子树为1(颠倒并不影响)。这样,根据权值(一般代表频率),可以得到最高效的二进制编码。
在代码中,我们设定char**为HuffmanCode的类型,原因是HuffmanCode是一个字符指针数组,他的每一个元素同样是字符指针,指向一个存储对应哈夫曼编码的字符数组。
代码如下。
void Create_HuffmanCode(HuffmanCode &HC,HuffmanTree &HT,int n)
{
HC=(HuffmanCode)malloc((n+1)*sizeof(char*));
char *cd;
cd=(char*)malloc(n*(sizeof(char)));
cd[n-1]='\0';
int start,p,c;
for(int i=1;i<=n;i++)
{
start=n-1;
c=i;
p=HT[c].parent;
while(p)
{
if(HT[p].lchild==c)
cd[--start]='0';
else
cd[--start]='1';
c=p;
p=HT[c].parent;
}
HC[i]=(char*)malloc((n-start)*(sizeof(char)));
strcpy(HC[i],&cd[start]);
}
free(cd);
}
其中malloc与free函数可以置换成new与delete,我们将编码先存放在cd数组里,最后根据cd数组长度为HC的每一个字符指针指向的数组动态分配空间,再用strcpy函数将内容复制到HC中。
里面start记录了编码位数,c代表循环里当前结点,p是其父节点,一直循环走下去,直到碰到根结点结束,路径终止,编码结束。
也不太难理解是吧。
其实哈夫曼树与编码并不抽象,属于比较简单的,但是也需要我们去用心揣测背后的细节,才能做到熟练掌握。
下面贴出全部代码,便于进行用例测试。
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include<string.h>
using namespace std;
#define MAX 100
#define status int
#define ElemType char
typedef struct
{
int weight;
int parent,lchild,rchild;
}HTNode,*HuffmanTree;
typedef char **HuffmanCode;
void input_weitht(int *w,int n)
{
cout<<"输入哈夫曼树结点的权值"<<endl;
for(int j=0;j<n;j++)
{
cin>>w[j];
}
}
void Select(HuffmanTree HT,int i,int &s1,int &s2)
{
int min1,min2;
for(int j=1;j<=i;j++)
{
if(HT[j].parent==0)
{
min1=j;
break;
}
}
for(int j=min1+1;j<=i;j++)
{
if(HT[j].parent==0)
{
if(HT[j].weight<HT[min1].weight)
min1=j;
}
}
for(int k=1;k<=i;k++)
{
if(HT[k].parent==0 && k!=min1)
{
min2=k;
break;
}
}
for(int k=min2+1;k<=i;k++)
{
if(HT[k].parent==0)
{
if(HT[k].weight<HT[min2].weight && k!=min1)
min2=k;
}
}
s1=min1;
s2=min2;
}
void Create_HuffmanTree(HuffmanTree &HT,int n,int *w)
{
int m,s1,s2;
m=2*n-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
for(int j=0;j<=m;j++)
{
HT[j].parent=HT[j].lchild=HT[j].rchild=0;
}
for(int j=1;j<=n;j++)
{
HT[j].weight=w[j-1];
}
for(int i=n+1;i<=m;i++)
{
Select(HT,i-1,s1,s2);
HT[s1].parent=i;
HT[s2].parent=i;
HT[i].lchild=s1;
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight;
}
}
void Create_HuffmanCode(HuffmanCode &HC,HuffmanTree &HT,int n)
{
HC=(HuffmanCode)malloc((n+1)*sizeof(char*));
char *cd;
cd=(char*)malloc(n*(sizeof(char)));
cd[n-1]='\0';
int start,p,c;
for(int i=1;i<=n;i++)
{
start=n-1;
c=i;
p=HT[c].parent;
while(p)
{
if(HT[p].lchild==c)
cd[--start]='0';
else
cd[--start]='1';
c=p;
p=HT[c].parent;
}
HC[i]=(char*)malloc((n-start)*(sizeof(char)));
strcpy(HC[i],&cd[start]);
}
free(cd);
}
void Show(HuffmanTree HT,int n)
{
int m=2*n-1;
cout<<"依次输出序号,weight,parent,lchild,rchild"<<endl;
for(int j=1;j<=m;j++)
{
cout<<"\t"<<j<<"\t";
cout<<HT[j].weight<<"\t";
cout<<HT[j].parent<<"\t";
cout<<HT[j].lchild<<"\t";
cout<<HT[j].rchild<<endl;
}
}
void show(HuffmanCode HC)
{
for(int i=1;i<=sizeof(HC);i++)
{
cout<<"序号为"<<i<<"的编码为:";
cout<<HC[i]<<endl;
}
}
int main()
{
HuffmanTree HT;
HuffmanCode HC;
int n;
cin>>n;
int w[MAX];
input_weitht(w,n);
Create_HuffmanTree(HT,n,w);
Create_HuffmanCode(HC,HT,n);
Show(HT,n);
show(HC);
}
里面的Show与show函数是简单的输出一下树结点与对应编码。
插入运行结果。
希望对大家有所帮助。
今天在完成老师的项目实验时,补充一段更加详细的代码吧,包含哈夫曼树的建立、编码以及译码的全过程,以菜单的形式展示。
与上面多出来的就是哈夫曼编码的破译函数。
void Encoding(HuffmanTree HT,char *s)
{
Get_Str(s);
int p;
cout<<"译码后的字符串为:";
while(*s!='\0')
{
p=2*n-1;
while(HT[p].lchild!=0 || HT[p].rchild!=0)
{
if(*s=='0')
p=HT[p].lchild;
else
p=HT[p].rchild;
s++;
}
cout<<HT[p].data;
}
cout<<endl;
}
破译的过程比较简单,就是基于已经建好的哈夫曼树,实质就是编码的逆过程。
我们从根节点开始,向下遍历,当编码为0时探索左子树,当编码为1时探索右子树,直到进入了孩子结点,该段编码被破译,此时返回的p就是对应的结点下标,我们直接输出其字符值即可。
其中参考我们建立哈夫曼树的过程,最终的根节点的下标是2n-1,也就是每轮循环p的初始值(没次破译一个字符后重新从根节点重复上述过程),直到编码末尾。
贴上一段项目完整代码,这里面内容会更加详细一些,包含了上面讲解的所有函数,感兴趣的小伙伴可以借助这段代码整体理解哈夫曼树里的一系列操作。
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include<string.h>
using namespace std;
#define MAX 100
#define status int
#define ElemType char
typedef struct
{
char data;
int weight;
int parent,lchild,rchild;
}HTNode,*HuffmanTree;
typedef char **HuffmanCode;
int n;
int get_num()
{
int n;
cout<<"输入结点个数,最大为100"<<endl;
cin>>n;
if(n<0 || n>100)
{
cout<<"个数不符合要求,请重新输入";
get_num();
return 0;
}
return n;
}
void input_weitht(int *w,int n)
{
cout<<"输入哈夫曼树结点的权值"<<endl;
for(int j=0;j<n;j++)
{
cin>>w[j];
}
}
void input_data(int n,char *data)
{
char ch;
cout<<"输入哈夫曼树对应字符的值"<<endl;
for(int j=1;j<=n;j++)
{
cin>>ch;
data[j]=ch;
}
}
void Select(HuffmanTree HT,int i,int &s1,int &s2)
{
int min1,min2;
for(int j=1;j<=i;j++)
{
if(HT[j].parent==0)
{
min1=j;
break;
}
}
for(int j=min1+1;j<=i;j++)
{
if(HT[j].parent==0)
{
if(HT[j].weight<HT[min1].weight)
min1=j;
}
}
for(int k=1;k<=i;k++)
{
if(HT[k].parent==0 && k!=min1)
{
min2=k;
break;
}
}
for(int k=min2+1;k<=i;k++)
{
if(HT[k].parent==0)
{
if(HT[k].weight<HT[min2].weight && k!=min1)
min2=k;
}
}
s1=min1;
s2=min2;
}
void Create_HuffmanTree(HuffmanTree &HT,int n,int *w,char *data)
{
int m,s1,s2;
m=2*n-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
input_data(n,data);
for(int j=1;j<=n;j++)
{
HT[j].data=data[j];
}
for(int j=0;j<=m;j++)
{
HT[j].parent=HT[j].lchild=HT[j].rchild=0;
}
for(int j=1;j<=n;j++)
{
HT[j].weight=w[j-1];
}
for(int i=n+1;i<=m;i++)
{
Select(HT,i-1,s1,s2);
HT[s1].parent=i;
HT[s2].parent=i;
HT[i].lchild=s1;
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight;
}
}
void Create_HuffmanCode(HuffmanCode &HC,HuffmanTree &HT,int n)
{
HC=(HuffmanCode)malloc((n+1)*sizeof(char*));
char *cd;
cd=(char*)malloc(n*(sizeof(char)));
cd[n-1]='\0';
int start,p,c;
for(int i=1;i<=n;i++)
{
start=n-1;
c=i;
p=HT[c].parent;
while(p)
{
if(HT[p].lchild==c)
cd[--start]='0';
else
cd[--start]='1';
c=p;
p=HT[c].parent;
}
HC[i]=(char*)malloc((n-start)*(sizeof(char)));
strcpy(HC[i],&cd[start]);
}
free(cd);
}
void Get_Str(char *s)
{
cout<<"请输入要编码的字符串"<<endl;
cin>>s;
}
void Encoding(HuffmanTree HT,char *s)
{
Get_Str(s);
int p;
cout<<"译码后的字符串为:";
while(*s!='\0')
{
p=2*n-1;
while(HT[p].lchild!=0 || HT[p].rchild!=0)
{
if(*s=='0')
p=HT[p].lchild;
else
p=HT[p].rchild;
s++;
}
cout<<HT[p].data;
}
cout<<endl;
}
void show(HuffmanCode HC,HuffmanTree HT)
{
for(int i=1;i<=sizeof(HC);i++)
{
cout<<HT[i].data<<"的编码为:";
cout<<HC[i]<<endl;
}
}
void Show(HuffmanTree HT,int n)
{
int m=2*n-1;
cout<<"依次输出序号,字符,weight,parent,lchild,rchild"<<endl;
for(int j=1;j<=m;j++)
{
cout<<"\t"<<j<<"\t";
cout<<HT[j].data<<"\t";
cout<<HT[j].weight<<"\t";
cout<<HT[j].parent<<"\t";
cout<<HT[j].lchild<<"\t";
cout<<HT[j].rchild<<endl;
}
}
void Init_HuffmanTree(HuffmanTree &HT)
{
int w[MAX];
char data[MAX];
n=get_num();
input_weitht(w,n);
Create_HuffmanTree(HT,n,w,data);
}
void sight()
{
cout<<"输入对应的操作数,0代表退出菜单"<<endl;
cout<<"1.初始化"<<endl;
cout<<"2.输出哈夫曼树"<<endl;
cout<<"3.输出哈夫曼编码"<<endl;
cout<<"4.译码"<<endl;
}
void Blank()
{
cout<<endl<<"/**********************/"<<endl;
}
void menu()
{
HuffmanTree HT;
HuffmanCode HC;
char s[MAX];
sight();
int m;
cin>>m;
while(m)
{
switch(m)
{
case 1:Init_HuffmanTree(HT);
break;
case 2:Show(HT,n);
break;
case 3:Create_HuffmanCode(HC,HT,n);
show(HC,HT);
break;
case 4:Encoding(HT,s);
}
Blank();
sight();
cin>>m;
}
}
int main()
{
menu();
}