一.基本概念
1.哈夫曼编码
在讲哈夫曼树之前先来介绍一下哈夫曼编码的概念。在信息编码、数据压缩等方面,我们总是希望编码能够尽可能的简短一点,如此才能节省空间,提高传输效率。例如:如果采用等长编码的话编码'abcd',则为00,01,10,11,此时占据8bit的空间,况且各个字符出现的频率还都为一次,如果出现多次则会效率很低;如果采用不等长编码的话则可以为:0,10,110,111,大家肯定会有一个问题:为什么要写成这种形式呢?如果我分别编码为0,1,00,01的话可以吗?那么假如编码串为"001011"的话计算机便不能进行唯一的解码了,因为此时可能的结果有'aababb','addb','cbdb',因此如果没有分隔符标注的话便会产生歧义。所以此时引入了一个前缀编码的概念——同一字符集中任何一个字符的编码都不是另外一个字符编码的前缀(最左字串),这种编码称为前缀编码。
在真正压缩的时候我们希望出现频率高的字符所占用较小的编码长度,如果之前你学过离散数学的话应该会听过最优二叉树的概念,即让权值小的元素靠近根结点,哈夫曼树便是这个原理。
2.哈夫曼树
路径:从树中一个结点到另一个结点之间的分支序列构成两个结点间的路径。
路径长度:路径上分支的条数称为路径长度。
树的路径长度:从树根到每个结点的路径长度之和称为树的路径长度。
结点的权:给树中结点赋予一个数值,该数值称为结点的权。
带权路径长度:结点到树根间的路径长度与结点权的乘积,称为该结点的带权路径长度。
树的带权路径长度:树中所有叶子结点的带权路径长度之和,称为树的带权路径长度,通常记为WPL
注:n为叶子结点个数(所要编码的字符数)
在这里,最优二叉树的写法不在赘述,具体可参考:https://jingyan.baidu.com/article/380abd0a717c061d90192ca2.html
二、哈夫曼树的建立与编译码
1.存储结构
哈夫曼树中没有度为1的结点,故n个叶子的哈夫曼树,恰有n-1个度为2的结点,所以哈夫曼树共有2n-1个结点,可以存储在一个大小为2n-1的一维数组中。
weight | Parent | Lchild | Rchild |
//结构声明
typedef struct point
{
int weight; //权值
int parent; //父节点下标
char data; //编码字母
int code;
int lchild,rchild;
}HuffmanNode;
2.建立哈夫曼树
1.在建立前我们需要对每个字符求其权值,在这里我们以字符出现的频度作为权值的衡量标准。
HuffmanNode *Compute_weight(HuffmanNode *p,int n) //计算权值
{
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
if(p[i]==p[j])
{
p[i].weight++;
}
}
}
return p;
}
2.利用其权值大小创建哈夫曼树
int Build_HuffmanTree(HuffmanNode *p,int n) //建立哈夫曼树
{
int s1,s2,*min;
for(int i=n+1;i<=2*n+1;i++)
{
min=SearchMinNumbers(p,i-1);
s1=*min;
s2=*(min++);
p[i].weight=p[s1].weight+p[s2].weight;
p[i].lchild=s1;
p[i].rchild=s2;
p[s1].parent=i;
p[s2].parent=i;
}
return 0;
}
3.哈夫曼树编码
此时我们规定左分支表示‘0’,右分支表示‘1’,用根结点到叶子结点路径上的分支符号组成的串,作为叶子节点字符的编码,这就是哈夫曼编码。
HuffmanNode ResolveHuffmanCode(HuffmanNode *p,int n) //打印字符编码
{
HuffmanNode ptemp;
char *code;
int start,pre;
code=(char *)mallco(sizeof(char)*n);
code[n-1]='\0';
for(int i=1;i<=n;i++)
{
start=n-1-1;
ptemp=i;
pre=p[i].parent;
while(pre)
{
if(p[pre].lchild==ptemp)
code[start]='0';
else
code[start]='1';
ptemp=pre;
pre=p[ptemp].parent;
start--;
}
}
}
4.哈夫曼树译码
哈夫曼译码过程较为简单,可按照编码表采用朴素字符串比对的方法进行解码,但是建议采用KMP算符进行字符串匹配,效率较高