摘 要
利用哈夫曼编码进行信息通信可大大提高信道利用率,缩短信息传输时间,降低传输成本。要求在发送端通过一个编码系统对待传数据预先编码;在接收端将传来的数据进行译码(复原)。对于双工信道(既可以双向传输信息的信道),每端都需要一个完整的编/译码系统。试为这样的信息收发站写一个哈夫曼的编/译码系统。
关键词:编码 C语言 哈夫曼 译码
第一章 设计目的
利用学过的数据结构知识设计一个简单的哈夫曼编/译码器系统。了解并掌握数据结构与算法的设计方法,具备初步的独立分析和设计能力;初步掌握软件开发过程的问题分析、系统设计、程序编码、测试等基本方法和技能;提高综合运用所学的理论知识和方法独立分析和解决问题的能力;训练用系统的观点和软件开发一般规范进行软件开发,培养软件工作者所应具备的科学的工作方法和作风。
- 需求分析
2.1 选题的意义和背景
利用哈夫曼编码进行通信可以大大提高信息利用率,缩短信息传输时间,降低传输成本。但是,这是要求在发送端通过一个编码系统对待传数据预先编码,在接收端将传来的数据进行译码,复原!。对于双工信道,即可以双向传输信息的信道!,每端都需要一个完整的编/译码系统。
2.2 基本要求
1) 将权值数据存放在数据文件(文件名为data.txt,位于执行程序的当前目录中)
2) 分别采用动态和静态存储结构
3) 初始化:键盘输入字符集大小n、n个字符和n个权值,建立哈夫曼树;
4) 编码:利用建好的哈夫曼树生成哈夫曼编码;
5) 输出编码;
6) 设字符集及频度如下表:
字符 空格 A B C D E F G H I J K L M
频度 186 64 13 22 32 103 21 15 47 57 1 5 32 20
字符 N O P Q R S T U V W X Y Z
频度 57 63 15 1 48 51 80 23 8 18 1 16 1
进一步完成内容:
1) 译码功能;
2) 显示哈夫曼树;
3) 界面设计的优化。
第三章 概要设计
3.1 设计思想
本程序的主要功能是实现对用户输入的字符编码,然后再把编码结果翻译成原字符。但在进行这些操作之前必须做一项工作,就是创建Huffman树。哈曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度,若根结点为0层,叶结点到根结点的路径长度为叶结点的层数!。树的带权路径长度记为WPL=(W1*L1+W2*L2+W3*L3+...+Wn*Ln),N个权值Wi(i=1,2,...n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,...n)。可以证明哈夫曼树的WPL是最小的。哈夫曼在上世纪五十年代初就提出这种编码时,根据字符出现的概率来构造平均长度最短的编码。它是一种变长的编码。在编码中,若各码字长度严格按照码字所对应符号出现概率的大小的逆序排列,则编码的平均长度是最小的。
3.2 程序框图及流程
哈夫曼编码器 编码 初始化 退出
3.3 方法及原理
3.3.1 创建Huffman树
本文创建Huffman树是在顺序链表的基础上进行的,建树原理如下(
1、根据给定的n个权值{w1,w2,w3,……,wn},构造具有n棵二叉树的森林F={T1,T2,T3,……,Tn},其中每棵二叉树Ti只有一个带权值wi的根结点,其左右子树均为空。
2、重复以下步骤,直到F中仅剩下一棵树为止(
(1)在F中选取两棵根结点的权值最小的二叉树,作为左右子树构造一棵新
的二叉树。使新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
(2)在F中删去这两棵二叉树,把新的二叉树加入F。
最后得到的就是Huffman树。
3.3.2 编码
编码操作需在建立好Huffman树的基础上进行。二叉树的叶子结点标记字符,由根结点沿着二叉树路径下行,左分支标记为0,右分支标记为1,则每条从根结点到叶子结点的路径唯一表示了该叶结点的二进制编码。编码的时候,我们采用从叶子结点向上回溯的方法编码,如果当前结点是其父结点的左孩子,则编码为0,如果是右孩子,则编码为1,如此回溯,直到父结点为空时,该字符的编码就结束了,对应编码结构中的编码数组就是该字符的编码。如此操作,直到所有叶子结点都扫描一遍为止,即编码结束。
3.4 主要的数据结构
3.4.1 Huffman结点结构
Huffman结点结构是本程序的基本结构,所有操作都在此上进行。其中包括存储字符的元素data,字符的权值weight,以及左右孩子指针和父指针。
typedef struct
{
char ch; //结点值
int weight; //权值
int parent; //父结点指针
int lchild; //左孩子结点指针
int rchild; //右孩子结点指针
}huffnode;
第四章 详细设计
4.1 创建Huffman树
4.1.1 功能描述
Huffman树是整个程序的核心部分,是编码译码操作的前提。哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。根据字符出现的概率来构造平均长度最短的编码。它是一种变长的编码。在编码中,若各码字长度严格按照码字所对应符号出现概率的大小的逆序排列,则编码的平均长度是最小的。
4.1.2 算法原理
首先根据用户输入创建n棵子树的森林,然后对所有子树扫描,找出权值最小的两个子树,把它们合并成一棵新的子树,同时把它们的权值之和作为新树的权值。把这两棵子树删掉,再把新树加如到森林中,然后再扫描出权值最小的两棵子树,接着进行同样的操作,直到只剩下一棵树即为Huffman树。
typedef char **hfmcode;
void Select(hfmtree &HT,int a,int *p1,int *p2) //Select函数;选出HT树到a为止;权值最小且parent为0的2个节点
{
int i,j,x,y;
for(j=1;j<=a;++j) {
if(HT[j].parent==0) {
x=j;
break;
}
}
for(i=j+1;i<=a;++i) {
if(HT[i].weight<HT[x].weight&&HT[i].parent==0) {
x=i; //选出最小的节点
}
}
for(j=1;j<=a;++j) {
if(HT[j].parent==0&&x!=j) {
y=j;
break;
}
}
for(i=j+1;i<=a;++i) {
if(HT[i].weight<HT[y].weight&&HT[i].parent==0&&x!=i) {
y=i; //选出次小的节点
}
}
if(x>y) {
*p1=y;
*p2=x;
}
else {
*p1=x;
*p2=y;
}
4.1.3 算法流程
流程图(一)
流程图
4.2 编码
4.2.1 功能描述
编码的功能就是把字符转换成二进制数来存储
4.2.2 算法原理
编码的时候,我们采用从叶子结点向上回溯的方法编码,如果当前结点是其父结点的左孩子,则编码为0,如果是右孩子,则编码为1,如此回溯,直到父结点为空时,该字符的编码就结束了,对应编码结构中的编码数组就是该字符的编码。如此操作,直到所有叶子结点都扫描一遍为止,即编码结束。
void hfmcoding(hfmtree &HT,hfmcode &HC,int n) //构建哈夫曼树HT;并求出n个字符的哈夫曼编码HC
{
int i,start,c,f,m,w; int p1,p2;
char *cd,z;
if(n<=1){
return;
}
m=2*n-1;
HT=(hfmtree)malloc((m+1)*sizeof(htnode));
for(i=1;i<=n;++i) //初始化n个叶子结
{
printf("请输入第%d字符信息和权值?",i); scanf("%c%d",&z,&w);
while(getchar()!='\n') {
continue;
}
HT[i].ch=z;
HT[i].weight=w;
HT[i].parent=0;
HT[i].lchild=0;
HT[i].rchild=0;
}
for(;i<=m;++i) //初始化其余的结点
{
HT[i].ch='0';
HT[i].weight=0;
HT[i].parent=0;
HT[i].lchild=0;
HT[i].rchild=0;
}
for(i=n+1;i<=m;++i) //建立哈夫曼树
{
Select(HT,i-1,&p1,&p2); HT[p1].parent=i;HT[p2].parent=i;
HT[i].lchild=p1;HT[i].rchild=p2;
HT[i].weight=HT[p1].weight+HT[p2].weight;
}
HC=(hfmcode)malloc((n+1)*sizeof(char *));
cd=(char *)malloc(n*sizeof(char));
cd[n-1]='\0';
for(i=1;i<=n;++i) //给n个字符编码
{
start=n-1;
for(c=i,f=HT[i].parent;f!=0;c=f,f=HT[f].parent) {
if(HT[f].lchild==c) {
cd[--start]='0';
}
else
{
cd[--start]='1';
}
}
HC[i]=(char*)malloc((n-start)*sizeof(char));
strcpy(HC[i],&cd[start]);
}
free(cd);
}
完整代码
#include<stdio.h>
#define n 8 //叶子结点数目
#define m (2*n-1) //总结点数目,可证明
#define MAXVALUE 10000 //最大权值
#define MAXBIT 20 //哈夫曼编码最大长度
typedef struct
{
char ch;
int weight;
int parent;
int Lchild, Rchild;
}Htreetype;
typedef struct
{
int bit[n]; //位串
int start; //编码在位串中的起始位置
char ch;
}Hcodetype;
void select(Htreetype t[], int k, int *p1, int *p2) //选择权值最小的结点
{
*p1 = *p2 = 0;
int small1, small2;
small1 = small2 = MAXVALUE;
int i;
for (i = 0; i < k; i++)
{
if (t[i].parent == -1)
{
if (t[i].weight < small1)
{
small2 = small1;
small1 = t[i].weight;
*p2 = *p1;
*p1 = i;
}
else if (t[i].weight < small2)
{
small2 = t[i].weight;
*p2 = i;
}
}
}
}
void HuffmanTree(Htreetype t[]) //构造哈夫曼树
{
int i, j, p1, p2, f;
p1 = p2 = 0;
char c;
for (i = 0; i < m; i++) //初始化
{
t[i].weight = 0;
t[i].Lchild = -1;
t[i].parent = -1;
t[i].Rchild = -1;
}
printf("共有%d个字符\n", n);
for (i = 0; i < n; i++) //输入字符和对应的权值
{
printf("请输入第%d个字符和权值','分隔", i + 1);
scanf("%c,%d", &c,&f);
getchar();
t[i].ch = c;
t[i].weight = f;
}
for (i = n; i < m; i++) //构造哈夫曼树
{
select(t, i, &p1, &p2);
t[p1].parent = i;
t[p2].parent = i;
t[i].Lchild = p1;
t[i].Rchild = p2;
t[i].weight = t[p1].weight + t[p2].weight;
}
}
void HuffmanCode(Hcodetype code[],Htreetype t[]) //哈夫曼编码
{
int i, c, p;
Hcodetype cd; //缓冲变量,暂时存储
HuffmanTree(t);
for (i = 0; i < n; i++)
{
cd.start = n;
cd.ch = t[i].ch;
c = i; //从叶子结点向上
p = t[i].parent; //t[p]是t[i]的双亲
while (p != -1)
{
cd.start--;
if (t[p].Lchild == c)
cd.bit[cd.start] = '0'; //左子树编为0
else
cd.bit[cd.start] = '1'; //右子树编为1
c = p; //移动
p = t[c].parent;
}
code[i] = cd; //第i+1个字符的编码存入code
}
}
void show(Htreetype t[], Hcodetype code[])
{
int i, j;
for (i = 0; i<n; i++)
{
printf("%c: ", code[i].ch);
for (j = code[i].start; j<n; j++)
printf("%c ", code[i].bit[j]);
printf("\n");
}
}
void Print()
{
printf("戴尔XPS-15\n");
}
int main()
{
Htreetype t[m];
Hcodetype code[n];
HuffmanCode(code, t);
show(t,code);
return 0;
}