哈夫曼编码与译码
一、哈夫曼编码定义
1.1、基本术语
路径: 从一结点到另一结点上的分支构成这两个结点的路径。
路径长度: 路径上的分支数目。
树的路径长度: 从根到所有结点的路径长度之和。
结点的带权路径长度: 从该结点到树根之间的路径长度与结点上权值的乘积。
树的带权路径长度: 树中所有叶子结点的带权路径长度之和。
1.2、哈夫曼树定义: 设有n 个权值 {w1,w2,......wn},试构造具有 n 个叶结点的二叉树,每个叶结点权值为 wi ,则其中带权路径长度WPL最小的二叉树称为哈夫曼树(最优二叉树)。特点:权值越大的叶子离根越近。若叶结点上的权值均相同,则完全二叉树一定是最优二叉树,否则完全二叉树不一定是最优二叉树。
WPL=2*(7+5+2+4)=36
WPL=3*(7+5)+2*4+2=46
WPL=3*(2+4)+2*5+7=35
1.3、哈夫曼树构造:
(1) 根据给定的n个权值 {w1,w2,......wn}, 生成 n 棵二叉树的集合F= {T1,T2,.......Tm};其中每棵二叉树Ti只有一个带权为Wi的根结点,左右子树为空。
(2) 在 F 中选择两棵根结点值最小的树 Ti ,Tj 作为左右子树,构成一棵新二叉树Tk , Tk根结点值为Ti ,Tj根结点权值之和;
(3) 在 F 中删除Ti ,Tj ,并把 Tk 加到 F中;
(4) 重复 (2) (3),直到 F中只含一棵树。
例:w={7,5,2,4}
1.4、哈夫曼编码
数据的压缩过程称为编码,解压过程称为解码。
编码:将文件中的字符转换为唯一的一个二进制串。
解码:将一个二进制串转换为对应的字符。
定长编码:设编码字符个数为n,码长为k,则k= 上界(log2^(n+1))。
不等长编码:使出现频率最多的字符采用尽可能短的编码。
前缀编码:对不等长编码,要求任一字符的编码都不是另一个字符的编码的前缀。
采用二叉树设计前缀编码:用二叉树的叶结点表示待编码的字符,并约定左分支表示字符‘0’,右分支表示字符‘1’,则从根结点到叶子结点的路径上分支字符组成的字符串作为该叶子结点的编码。由此得到的编码必为二进制的前缀编码。
方法:以n种字符出现的频率作权,设计一棵哈夫曼树,由此得到字符的二进制前缀编码为总长最短的二进制前缀编码,这种编码即为哈夫曼编码。
二、实验实现
2.1、实验内容
哈夫曼编码生成与译码。
输入符号(序号用英文字母A, B, C, …表示)以及各符号出现概率,以字符串形式输出各符号对应的二进制哈夫曼编码。
以字符串形式输入接收到的比特序列,输出译码后的符号序列。建议用菜单形式提供功能。
2.2、输入与输出
输入:编码的个数,编码的权重,编码的符号,接收到的比特序列。
输出:编码结果和译码结果。
2.3.关键数据结构与算法描述
关键数据结构:霍夫曼树的节点构造以及权重表,符号表,编码表,实际符号数。由于采用数组的形式来存储所有节点,则要提供左右子树和双亲节点指针。
1 typedef char ElemType;
2 typedef struct {
3 int parent; //双亲下标
4 int lchild; //左儿子下标
5 int rchild; //右儿子下标
6 double w; //结点权重
7 }HF_BTNode; //码树结点类型
8 typedef struct
9 {
10 int n; //实际符号数, n<=N
11 ElemType s[N]; //符号表
12 double weight[N]; //符号权重表
13 char code[N][N+1]; //编码表
14 HF_BTNode hf[2*N-1]; //码树
15 } HFT; //Huffman码树及码表
算法描述:
首先是哈夫曼树的生成需要根据相应的数据结构采用相应的算法。因为采用的是数组存储树的节点,属于顺序存储结构。首先根据算法应该先找到所有根节点中最小的两个组成一棵新树的左右子树,删除这两个节点(此处用parent为-1来说明为根节点,如不是-1,则为删除),添加新生成的节点即让新树的根节点的parent为-1即可。因此根据思路可以遍历要生成节点前的所有节点来不断生成新树,最后形成只有二度节点和零度节点的二叉树。具体算法如下:
int m=2*a.n-1; //所有节点数
for(int i=0; i<m; i++)//对所有节点
{
a.hf[i].parent=a.hf[i].lchild=a.hf[i].rchild=-1;//-1代表着为根节点
}
for(i=0; i<a.n; i++)
{
a.hf[i].w=a.weight[i]; //权重赋值
}
/**********************生成霍夫曼树***********************************/
int j1,j2,j;
for(i=a.n; i<m; i++) //从原有节点之后增添数值
{
for(j1=0; j1<i; j1++) //遍历带增添节点之前所有节点找到根节点
{
if(a.hf[j1].parent==-1)
{
break;
}
}
for(j=j1+1; j<i; j++)//找到最小的根节点作为新增节点的左子树
{
if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j1].w)
{
j1=j;
}
}//j1为最小节点
a.hf[j1].parent=i;
a.hf[i].lchild=j1;
for(j2=0; j2<i; j2++) //同理找到次小节点,作为新增节点右子树
{
if(a.hf[j2].parent==-1)
{
break;
}
}
for(j=j2+1; j<i; j++)
{
if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j2].w)
{
j2=j; //j2为次小节点
}
}
a.hf[j2].parent=i;
a.hf[i].rchild=j2;
a.hf[i].w=a.hf[j1].w+a.hf[j2].w;//节点生成完成,权重赋值
}
然后,就要进行编码了,对生成的霍夫曼树约定左边编1,右边编0,只需从符号表亦即最原始节点开始追溯到根,即可的反序的编码,然后将其翻转即可。具体代码如下:
/*********************生成code字符串数组*******************/
for(i=0; i<a.n; i++)
{
int j=i;
char *p=a.code[i],*q; //j从第i个叶子出发上溯至根结点,故编码表一定和字符表对应
while(a.hf[j].parent!=-1)//未到根节点时持续编号
{
int child=j;
j=a.hf[j].parent;
if(a.hf[j].lchild==child)
{
*p++='1';//左边是1
}
else
{
*p++='0';//右边是0
}
}
*p='