前言
哈夫曼编码是一种结合二叉树与贪心算法的文字、数据编码方式。具有较广的应用背景,最直观的应用便是文件压缩。举例说明,有一串多个字符组成的字符串“abbcccddddeeeee”,该字符串包含有五个字符,abcde五个字符分别出现的次数为1,2,3,4,5.。由于一个字符需要由多个二进制位进行表示,为了不混淆字符,需要用3个二进制位来表示字符才可以确保5种不同的组合(),这串字符一共需要3*15=45位来表示。将信息一个不落地表示需要满足每一个单元信息的互异性,同时也要按原有的顺序来将信息写出。而这串信息中每个字符的哈夫曼对应为 a-010 b-011 c -00 d-10 e-11,此时的字符串需3*1+3*2+2*3+2*4+2*5=33位来表示。通过将高频率字符用较短字节进行表示而不是统一表示的方法达到字符串的压缩。
正文
1.哈夫曼编码的概念
哈夫曼树又称作最优树,是一种带权路径长度最短的树,而通过哈夫曼树构造出的编码方式称作哈夫曼编码。先补充以下三个概念:
1)路径长度
二叉树中一个结点到另一个结点经过的结点方式是唯一的,这些经过的结点称作两个结点之间的路径。而由结点A到结点B中间经历的结点数目加1即为结点A到B的路径长度。
2)结点权值
图当中,每一个结点代表着不同的数字,而这些不同的数字称作结点的权值。
3)带权路径长度
假设同样的例子,由结点A到结点B的过程中,经过的路径长度为3,结点B的权值为4。则3*4=12称作结点的带权路径长度。到结点的带权路径长度计算方式为路径长度乘结点的权值。而树的带权路径长度,意为树的根结点到所有的叶子结点的带权路径长度之和。表达公式如下:
WPL代表树的带权路径长度,代表树叶子结点的权值,代表根结点到每个叶子结点的路径长度。
哈夫曼树必须要求WPL达到最短,而哈夫曼编码的编写方式,则要求将所有需要编码的元素放置在哈夫曼树的叶子结点上,方便编码。
2.哈夫曼编码的构造方法
哈夫曼编码在编写的过程中,要求先构造哈夫曼树。遵照的是权值最小原则,以前言中提到的字符串为例,字符 a的权值赋值为1,字符b的权值赋值为2,c的权值赋值为3,d的权值赋值为4,e的权值赋值为5。
1)选取权值最小的两个点1 2,将两个点作为叶子结点。1作为左孩子,2作为右孩子构造一棵新二叉树,根结点的权值为两叶子结点之和3。
2)将新的根结点作为叶子结点考虑,比较权值为{3,3,4,5}的四个结点,得出权值最小的为3和3。与第一步类似,以这两个点作为叶子结点,构造一棵新二叉树,根结点的权值为两叶子结点之和6。
3)此时集合为{4,5,6}三个结点,此处集合权值最小的两个点为 4 5,则将4,5作为叶子结点。4作为左孩子,5作为右孩子构造一棵新的二叉树,根结点权值为9
4)最后集合为{6,9},将两个点合并为一棵二叉树。6作为左孩子,9作为右孩子,根结点为15。
构造哈夫曼树的情况如下:
构造完毕哈夫曼树后,由根结点开始对叶子结点进行编码,要求根结点的左子树添加的数字为0,右子树添加的数字为1,将左右子树分别作为根结点继续向下添加编码。一直添加数字到每个叶子结点为止。叶子结点对应的数字是这个叶子元素的哈夫曼编码。编码结果为: a-010 b-011 c -00 d-10 e-11,哈夫曼编码的过程如下:
3.哈夫曼编码构造的代码实现
在了解了哈夫曼编码的原理后,需要开始进行哈夫曼编码的实现,实现过程中主要有三个难点,其一是二叉树的构造,其二是最小值的挑选,其三是一维的数组如何转换为二叉树的结构。可以对节点进行编号,用编号来访问具有父子关系的结点。基本思路如下:
1)分析哈夫曼树的结构以及结点数目关系
假设哈夫曼树具有m个结点,叶子结点的数目为n,则不难得出 m=2*n-1 。建立一个哈夫曼树类的结点,其中类的变量包括权值weight,左孩子的下标lchild,右孩子的下标rchild,父结点的下标parent,类的一个构造函数记作HTNode(int w,int p,int l,int r)。用于进行哈夫曼树结点的初始化以及赋值。同链表的想法类似,类的单个对象记作HTnode,定义一个类的指针对象HuffmanTree.
类的代码如下:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
using namespace std;
typedef struct HTNode
{
int weight;
int parent;
int lchild;
int rchild;
HTNode(int w,int p,int l,int r):weight(w),parent(p),lchild(l),rchild(r){}
}HTnode,*HuffmanTree;
2)函数基本结构的实现
定义一个哈夫曼编码加密函数Huffmancoding(),其中函数的自变量包括HuffmanTree类型的对象数组HT,定义一个二维的字符指针Huffmancode,Huffmancode的对象名称记作HC。分成多行,用于存储abcde五个待编码的字符,每一个字符都对应的编码均存储在Huffmancode的每一行中。定义一个int型指针,记作w,w指向的数组用于存储每一个字符的权值。最后需要定义变量n,记录下叶子结点的数目,同时根据叶子结点的数目也可以推断出包括根结点在内的所有结点的数目。函数声明如下:
typedef char**Huffmancode;
void Huffmancode(HuffmanTree &HT,Huffmancode &HC,int *w,int n);
在定义完成该输入的变量后,定义int类型的变量记作m与i。m代表结点的数目,同时也控制着HT对象内存的分配,i控制着循环。对于下标为1到n的结点,控制它们承担叶子结点的信息,相应地,初始化时将结点的权值与w中的各个数字相互对应,父子结点以及左右孩子的信息均初始化为0。对于下标n+1到m的结点,初始化值均设为0。HT对象内存分配以及叶子结点初始化代码如下:
m=n*2-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
for(i=1;i<=n;i++,w++)
{
HT[i]=HTNode(*w,0,0,0);
}
for(;i<=m;i++)
{
HT[i]=HTNode(0,0,0,0);
}
在结点初始化完成后,需要完善的是对应父子结点的信息,对于不同下标的结点进行结点内部元素的赋值,达到构建二叉树的效果。
假设存在一个待实现的Select函数,找到权值最小的两个点的下标,实现放到3)实现Select函数来讲,两个权值最小的结点下标为child1,child2。通过for循环,i 的值初始化为n+1,最后到达m为止,i 每一次都增加1。将HT[i]作为HT[child1]与HT[child2]的父亲结点,将对应的权值以及左右孩子值均赋上。通过for循环中存在的 i+1操作将新产生的HT[i]结点加入待寻找权值最小的序列中。实现代码如下:
for(i=n+1;i<=m;i++)
{
Select(HT,i-1,child1,child2); //i-1=n时代表前n个结点中找权值最小的两个点,i++使得i=n+1
HT[child1].parent=i; //时的新产生的父结点被包括进来
HT[child2].parent=i;
HT[i].lchild=child1;
HT[i].rchild=child2;
HT[i].weight=HT[child1].weight+HT[child2].weight;
}
寻找了权值最小的两个结点HT[child1],HT[child2]后,需要开始对待编码的字符进行编码。这里主要是运用了向上回溯的思想,通过对每个叶子结点的访问来达到编码的效果。给HC分配空间,定义字符指针cd,用来存储每一个字母对应的编码值。按照访问叶子结点的思路,先通过外层for循环来访问每一个叶子结点。访问叶子结点后,通过内层for循环向上回溯,添加内外for循环的语句如下:
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n)
{
int m,i;
int child1,child2;
if(n<=1)
{
return;
}
m=n*2-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
for(i=1;i<=n;i++,w++)
{
HT[i]=HTNode(*w,0,0,0);
}
for(;i<=m;i++)
{
HT[i]=HTNode(0,0,0,0);
}
for(i=n+1;i<=m;i++)
{
Select(HT,i-1,child1,child2);
HT[child1].parent=i;
HT[child2].parent=i;
HT[i].lchild=child1;
HT[i].rchild=child2;
HT[i].weight=HT[child1].weight+HT[child2].weight;
}
HC=(char**)malloc((n+1)*sizeof(char*));
char*cd=(char*)malloc(n*sizeof(char));
int temp,count;
for(i=1;i<=n;i++)
{
count=0;
for(int c=i,temp=HT[i].parent;temp!=0;c=temp,temp=HT[temp].parent)
{
if(HT[temp].lchild==c)
{
cd[count++]='0';
}
else
{
cd[count++]='1';
}
}
cd[count]='\0';
}
}
其中c代表当前访问结点的下标,temp代表该结点父结点的下标,若当前结点是它父结点的左孩子,则在cd中添加上字符 ‘0’,若是右孩子,则在cd中添加上字符 ‘1’。每经过一次叶子结点的编码后cd均会更新。需要注意的是,将cd写入HC的每一行中时,需要将cd反向写入(由叶子结点向上回溯得到的编码是相反的),完整编码的代码如下:
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n)
{
int m,i;
int child1,child2;
if(n<=1)
{
return;
}
m=n*2-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
for(i=1;i<=n;i++,w++)
{
HT[i]=HTNode(*w,0,0,0);
}
for(;i<=m;i++)
{
HT[i]=HTNode(0,0,0,0);
}
for(i=n+1;i<=m;i++)
{
Select(HT,i-1,child1,child2);
HT[child1].parent=i;
HT[child2].parent=i;
HT[i].lchild=child1;
HT[i].rchild=child2;
HT[i].weight=HT[child1].weight+HT[child2].weight;
cout<<child1<<" "<<child2<<endl;
}
HC=(char**)malloc((n+1)*sizeof(char*));
char*cd=(char*)malloc(n*sizeof(char));
int temp,count;
for(i=1;i<=n;i++)
{
count=0;
for(int c=i,temp=HT[i].parent;temp!=0;c=temp,temp=HT[temp].parent)
{
if(HT[temp].lchild==c)
{
cd[count++]='0';
}
else
{
cd[count++]='1';
}
}
cd[count]='\0';
HC[i]=(char*)malloc((count)*sizeof(char));
for(int j=count;j>=0;j--)
{
HC[i][count-j]=cd[j-1];//cd反向输入
}
}
}
3)实现Select函数
按已经实现的想法来看,需要完成一个函数以对权值最小的两个结点进行合并,用一个函数Select完成对这两个权值最小结点的寻找,在其他函数中再对其进行实现。进行Select函数的函数自变量包括哈夫曼树对象HT,待寻找的结点集合下标最大值Num,以及两个权值最小的结点的下标child1,child2。经过此函数的目的,是将寻找到正确的child1与child2的值,使得在该次循环中HT[child1]与HT[child2]的权值达到最小。
基本思路如下:1.判断HT[i].parent是否为0,若不为0,则代表这个结点已经被归入一棵树内,不能再次进行合并,为0则可以继续 2.child1,child2,w1,w2均初始化为0 3.通过(w1>w2)或(w2>w1)在 i 从 1 至Num中反复找最小权值的两个点,在多次的循环中child1与child2会反复更新,并且(w2>w1)条件在后保证了HT[child1]的权值要小于HT[child2]的权值。代码如下:
void Select(HuffmanTree &HT,int num,int &child1,int &child2)
{
child1=0;
child2=0;
int w1=0;
int w2=0;
for(int i=1;i<=num;i++)
{
if(HT[i].parent==0)
{
if(child1==0)
{
child1=i;
w1=HT[i].weight;
continue;
}
if(child2==0)
{
child2=i;
w2=HT[i].weight;
continue;
}
if(w1>w2&&w1>HT[i].weight)
{
w1=HT[i].weight;
child1=i;
continue;
}
if(w2>w1&&w2>HT[i].weight)
{
w2=HT[i].weight;
child2=i;
continue;
}
}
}
}
4)主函数的实现
相对以上步骤,主函数便极其容易,这里直接将 abcde 五个字符的权值存在数组中,直接输出了每个字符的编码,主程序如下:
int main()
{
char a[]={'a','b','c','d','e'};
int b[]={1,2,3,4,5};
HuffmanTree HT;
HuffmanCode HC;
HuffmanCoding(HT,HC,b,5);
for(int j=1;j<=5;j++)
{
cout<<a[j-1]<<" "<<HC[j]<<endl;
}
return 0;
}
编码结果如下所示:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
using namespace std;
typedef struct HTNode
{
int weight;
int parent;
int lchild;
int rchild;
HTNode(int w,int p,int l,int r):weight(w),parent(p),lchild(l),rchild(r){}
}HTnode,*HuffmanTree;
typedef char** HuffmanCode;
void Select(HuffmanTree &HT,int num,int &child1,int &child2)
{
child1=0;
child2=0;
int w1=0;
int w2=0;
for(int i=1;i<=num;i++)
{
if(HT[i].parent==0)
{
if(child1==0)
{
child1=i;
w1=HT[i].weight;
continue;
}
if(child2==0)
{
child2=i;
w2=HT[i].weight;
continue;
}
if(w1>w2&&w1>HT[i].weight)
{
w1=HT[i].weight;
child1=i;
continue;
}
if(w2>w1&&w2>HT[i].weight)
{
w2=HT[i].weight;
child2=i;
continue;
}
}
}
}
//HuffmanCode是一个二维数组,用于存储字符
//HuffmanTree是一棵哈夫曼树
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n)
{
int m,i;
int child1,child2;
if(n<=1)
{
return;
}
m=n*2-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
for(i=1;i<=n;i++,w++)
{
HT[i]=HTNode(*w,0,0,0);
}
for(;i<=m;i++)
{
HT[i]=HTNode(0,0,0,0);
}
for(i=n+1;i<=m;i++)
{
Select(HT,i-1,child1,child2);
HT[child1].parent=i;
HT[child2].parent=i;
HT[i].lchild=child1;
HT[i].rchild=child2;
HT[i].weight=HT[child1].weight+HT[child2].weight;
//cout<<child1<<" "<<child2<<endl;
}
HC=(char**)malloc((n+1)*sizeof(char*));
char*cd=(char*)malloc(n*sizeof(char));
int temp,count;
for(i=1;i<=n;i++)
{
count=0;
for(int c=i,temp=HT[i].parent;temp!=0;c=temp,temp=HT[temp].parent)
{
if(HT[temp].lchild==c)
{
cd[count++]='0';
}
else
{
cd[count++]='1';
}
}
cd[count]='\0';
//cout<<cd<<" "<<i<<endl;
HC[i]=(char*)malloc((count)*sizeof(char));
for(int j=count;j>=0;j--)
{
HC[i][count-j]=cd[j-1];
}
}
}
int main()
{
char a[]="abbcccddddeeeee";
char c[]={'a','b','c','d','e'};
int i=strlen(a);
int b[]={1,2,3,4,5};
HuffmanTree HT;
HuffmanCode HC;
HuffmanCoding(HT,HC,b,5);
for(int j=1;j<=5;j++)
{
cout<<c[j-1]<<" "<<HC[j]<<endl;//此处似乎是用来进行字符的编码的?输出abcd四个字符所对应的编码
}
return 0;
}