目录
一. 前言
二. 哈夫曼树的构造
三. 哈夫曼编码
一. 前言
在学习哈夫曼树之前,我们先了解几个基本概念。
1.路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。
2.结点的路径长度:两结点间路径上的分支数。
3.树的路径长度:从树根到每一个结点的路径长度之和。记作:TL。
4.通过分析可以知道,结点数相同的二叉树中,完全二叉树是路径长度最短的二叉树。但路径长度最短的二叉树不一定是完全二叉树。
5.权:将树中结点赋给一个有着某种含义的数值,这个数值称为该结点的权。
6.结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
7.树的带权路径长度:树中所有叶子结点的带权路径长度。
在有了上面的知识作基础之后,我们就能理解哈夫曼树的定义了。
所谓哈夫曼树就是指带权路径长度最短的树,也叫最优树。当它为最优二叉树的时候就是指的带权路径长度(WPL)最短的二叉树。
并且我们可以知道它的特点,就是满二叉树不一定是最优二叉树。最优二叉树中权越大的数离根越近。并且具有相同带权结点的哈夫曼树不唯一。
二. 哈夫曼树的构造
实现哈夫曼树的算法步骤可以分为以下几步:
1)首先根据n个给定的权值来构造n棵二叉树的森林F。森林F中的每一棵树都只有一个结点,也就是根结点。可以使用这样一个口诀来加以记忆这一步骤:构造森林全是根。
2)接着在这个森林F中选取两棵根结点的权值最小的树作为左右子树,用来构造一棵新的二叉树。并且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。记忆口诀为:选用两小造新树。
3)然后在这个森林F中删除这两棵树,同时将新得到的二叉树加入到这个森林F中。记忆口诀为:删除两小添新人。
4)重复2)3)两个步骤,直到森林中只有一棵树为止,这棵树就是哈夫曼树。
我们可以看下面一个例子来巩固掌握哈夫曼树的实现:
首先就是构造森林全是根。然后找到权值最小的两棵树,也就是d和e,它们的权值最小为2和4。将它们构成一个新的二叉树加入到森林中,然后重复这两步的操作。得到的最后只有一棵树,也就是我们所求的哈夫曼树。
并且通过分析,我们可以发现,假设初始时有n棵二叉树,其中的二叉树两两进行合并,那么就需要经过n-1次合并最终才形成哈夫曼树,经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支结点。
由此可见,哈夫曼树中一共有n+n-1=2n-1个结点,且其所有的分支结点的度均不为1。
在知道了哈夫曼树的原理和实现之和,我们该怎么用代码表示出来呢?
下面我将给出哈夫曼构造算法的完整代码,包括找两个最小权值的根结点(也就是还没合并的两个二叉树,还没有双亲)的函数,以及打印哈夫曼树中序号为1的结点数据。同学们也可自行在我的代码基础上添加一些其他功能。
//构造哈夫曼树的结点类型
typedef struct HTNode{
int weight; //权值
int parent,lchild,rchild; //结点的左右孩子以及双亲的位置序号
}HTNode,*HuffmanTree;
//函数的声明
void Choice(HuffmanTree HT,int n,int&s1,int&s2);
//最后结果会返回一个哈夫曼树的函数
HuffmanTree CreatHuffmanTree(int n){
if(n<=1) exit; //如果结点数小于2,会退出
m=2*n-1; //带权的结点数为n,那么整个哈夫曼树就会有2*n-1个结点。
HuffmanTree HT=new HTNode[m+1]; //一般不使用序号为0的位置,所以m再加1
for(int i=1;i<=m;i++){ //将里面所有的元素的这些属性先赋值为0
HT[i].lchild=0;
HT[i].rchild=0;
HT[i].parent=0;
}
for(int i=1;i<=n;i++){ //输入前n个树的权值
scanf("%d",&HT[i].weight);
}
for(i=n+1;i<=m;i++){ //开始进行合并,总共合并n-1次
Choice(HT,i-1,s1,s2); //这个choice函数就是在HT[k](1<=k<=i-1)中选择两个双亲为0,也就是没有被合并的根结点,并且权值最小的结点,返回它们在HT中的序号s1和s2.
HT[s1].parent=i; //其中序号为s1的根结点有了双亲
HT[s2].parent=i; //跟s1是兄弟关系
HT[i].lchild=s1; //给这个n+1的位置上的树分配左右孩子
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight; //权值为这两个子树之和
}
return HT; //在完成了上面的哈夫曼树构造之后,返回一个哈夫曼树
}
void Choice(HuffmanTree HT,int n,int&s1,int&s2){
int min1=10000;
int min2=10000;
s1=s2=0; //先给这两个变量初始化
for(int i=1;i<=n;i++){
if(HT[i].parent==0&&HT[i].weight<min1){
//用min1来存放其中权值最小的,min2来存放其中权值第二小的。所以一旦有比min1还要小的数,就先
//将min1现在的值赋值给min2,接着再把这个更小的数赋值给min1.
min2=min1;
s2=s1;
min1=HT[i].weight;
s1=i;
}
else if(HT[i].parent==0&&HT[i].weight<min2){ //只比第二小的数小,则只修改min2的值
min2=HT[i].weight;
s2=i;
}
}
}
int main(){
int n=4; //带权的结点数为4
HuffmanTree HT=CreatHuffmanTree(n); //用HT来接收在CreatHuffmanTree函数中已经构造好了的哈夫曼树
printf("%d",HT[1].weight); //简单测试下,打印一号位置的权值
}
三. 哈夫曼编码
哈夫曼编码就是将要发送的数据(可能是数字也有可能是字符)转换成由二进制组成的字符串。举个简单例子:例如发送ABC,将A的编码设置成1,B的编码设置成2,C的编码设置成3。那么最后发送的就是123。哈夫曼编码要求传送的字符串中出现次数较多的字符采用尽可能短的编码,那么转换成的二进制字符串便可能减少,并且是前缀编码。例如1是2的前缀编码,而0不是01的前缀编码,会造成重码的问题。
实现哈夫曼编码的方法:
在知道了哈夫曼编码的原理和方法之后,我们如何用代码实现呢?
如下所示:
//从叶子到根逆向求每一个字符的哈夫曼编码,并存储到编码表HC中
void CreatHuffmanCode(HuffmanTree HT,HuffmanCode&HC,int n){
HC=new char*[n+1]; //分配n个字符编码的头指针矢量
cd=new char[n]; //分配临时存放编码的动态数组空间
cd[n-1]='\0'; //最后一个位置存放结束符,编码最长为n-1,因此空间不会小
for(int i=1;i<=n;i++){ //逐个字符求哈夫曼编码
int start=n-1;
int c=i;
int f=HT[i].parent;
while(f!=0){ //从叶子结点开始往上回溯,直到根结点
--start;
if(HT[f].lchild==c) cd[start]='0'; //结点c是f的左孩子,则生成代码0
else cd[start]='1'; //右孩子生成代码1
c=f; //继续向上回溯
f=HT[f].parent;
}
HC[i]=new char[n-start]; //为第i个字符的编码分配空间
strcpy(HC[i],&cd[start]); //将所求得编码从临时空间cd中赋值到HC当前位置上
}
delete cd; //释放临时空间
}