实验三:哈夫曼编/译码器
实验目的: 掌握哈夫曼树
实验内容: 利用哈夫曼编码进行通信可以大大提高信道利用率,缩短信息传输时间,降低传输成本。但是,这要求在发送端通过一个编码系统对待传数据预先编码,在接收端将传来的数据进行译码(复原)。对于双工信道(即可以双向传输信息的信道),每端都需要一个完整的编/译码系统。试为这样的信息收发站写一个哈夫曼码的编/译码系统。
实验要求: 编写完整的系统,要求具有以下功能:
I:初始化(Initialization)。从终端读入字符集大小n,以及n个字符和n个权值,建立哈夫曼树,并将它存入文件hfmTree中。
E:编码(Encoding)。利用以建好的哈夫曼树(如不在内存,则从文件hfmTree中读入),对文件ToBeTran中的正文进行编码,然后将结果存入文件CodeFile中。
D:译码(Decoding)。利用已建好的哈夫曼树将文件CodeFile中的代码进行译码,结果存入文件TextFile中。
P:印代码文件(Print)。将文件CodeFile以紧凑格式显示在终端上,每行50个代码。同时将此字符形式的编码文件写入文件CodePrin中。
T:印哈夫曼树(Tree printing)。将已在内存中的哈夫曼树以直观的方式(树或凹入表形式)显示在终端上,同时将此字符形式的哈夫曼树写入文件TreePrint中。
实验过程
-
需求分析
首先要确定n个字符及其权值。一篇英语短文包括字母的大小写以及基本的符号,从网上查找资料获得各个字母出现的平均概率,作为各个符号的权值。由于符号众多,我们选择将其保存到文件里面进行读取。
其次,关于文件的写入操作。我们直接将屏幕上打印出来的内容保存到相应的txt文件中去。
最后树的打印,我们采取中序遍历树的方式,将一棵树横向的打印到文件中去。 -
概要设计
相关的数据结构设计:
哈夫曼树的节点typedef struct Node { int weight;//权值 int parent;//父节点的序号,为0表示根节点 int lchild, rchild;//左右孩子节点的序号,为0的是叶子节点 }HTNode, *HuffmanTree;
编译结果的保存
struct Ans { char ch; // 保存字符 int wet; // 保存权值 }ans[maxn];
主程序的流程:
- 初始化:从文件中读取权值和字符,建立一棵哈夫曼树。
- 编码:编写函数HuffmanCoding确定每个字符的哈夫曼编码。
- 译码:利用事先准备好的一篇文章,打印出文章结果。
- 打印部分:打印包括代码和哈夫曼树。
-
详细设计
- 建立哈夫曼树的过程
HuffmanTree create_HuffmanTree(struct Ans *p, int n) { int t = 2 * n - 1; HuffmanTree HT = (HuffmanTree)malloc(total * sizeof(HTNode)); int i; for (i = 0; i < n; ++i) {//先构造n个叶节点 HT[i].parent = 0;//parent后面会改变 HT[i].lchild = 0; HT[i].rchild = 0; HT[i].weight = p->wet; p++; } for (; i < total; ++i) { HT[i].parent = 0; HT[i].lchild = 0; HT[i].rchild = 0; HT[i].weight = 0; } int min1, min2;//用来保存每一轮选出的两个weight最小且parent为0的节点下标 //每一轮比较后选择出min1和min2构成一课二叉树,最后构成一棵哈夫曼树 for (i = n; i < t; ++i) { select_min(HT, i, min1, min2); //另外编写的一个函数,用来寻找i之前的结点 HT[min1].parent = i; HT[min2].parent = i; //两个最小权值的结点构成子树 HT[i].lchild = min1; HT[i].rchild = min2; HT[i].weight = HT[min1].weight + HT[min2].weight; } /* for(int i=0;i<total;i++) { printf("%d-%d-%d\n",HT[i].lchild,HT[i].rchild,HT[i].weight); printf("%c\n",ans[i].ch); }*/ return HT; }
- 译码过程
void HuffmanCoding(HuffmanTree HT, int n) { char temp[maxn + 5];//临时用来保存每次求得的一个哈夫曼编码串 temp[maxn] = '\0';//结束符 for (int i = 0; i < n; ++i) { int current = i; int fa = HT[i].parent; int start = maxn; //初始为编码结束符的位置 while (fa != 0) { if (HT[fa].lchild == current) temp[--start] = '0'; else temp[--start] = '1'; current = fa; fa = HT[current].parent; } strcpy(HC[i], temp + start);//将编码串从最后的start编码~\0复制到HC上 } }
- 初始化(从文件中读入权值)
int n;//需要编码的字符数 void Initialzation() { FILE *fp1,*fpx; fp1 = fopen("./DataFile.data", "r"); printf("从文件DataFile.txt中读取编码的字符种类个数\n"); fscanf(fp1, "%d", &n); printf("以及这%d个字符及其权值(整型)\n", n); for (int i = 0; i < n; ++i) { fscanf(fp1, " %c %d", &ans[i].ch, &ans[i].wet); } HuffmanTree HT = create_HuffmanTree(ans, n); HuffmanCoding(HT, n);//求解每个字符的编码 printf("各字符对应权值的哈夫曼编码为(按照输入顺序):\n"); for (int i = 0; i < n; ++i) { printf("%c :", ans[i].ch); puts(HC[i]); } printf("将它们存入htmtree.txt中去:\n"); fpx=fopen("./htmtree.txt","w+"); for(int i=0;i<n;i++) { fprintf(fpx,"%c %s\n",ans[i].ch,HC[i]); } fclose(fp1); fclose(fpx); printf("\n******************************************************\n\n"); }
- 编码
void EnCoding() { //char ch[maxn*maxn],temp; printf("下面对文件ToBeTran.data中的文本进行编码形成报文\n"); printf("文章写在文件CodeFile.txt中\n"); FILE *fp2, *fp3; fp2 = fopen("ToBeTran.txt", "rb"); fp3 = fopen("CodeFile.txt", "w"); //fscanf(fp2,"%s",temp); char ch; do { ch = fgetc(fp2); /* read a char from the file */ if (ch == ' ') fprintf(fp3, " "); else if (ch == '\n') fprintf(fp3, "\n"); else if(ch-'a'>=0) fprintf(fp3, "%s", HC[ch - 'a']); else if(ch-'A'>=0) fprintf(fp3, "%s", HC[ch - 'A'+26]); else if(ch-'0'>=0&&'9'-ch>=0) fprintf(fp3, "%s", HC[ch - '0'+52]); else if(ch==',') fprintf(fp3, "%s", HC[62]); else if(ch=='.') fprintf(fp3, "%s", HC[63]); } while (ch != EOF); fclose(fp2); fclose(fp3); for(int i=0;i<n;i++) printf("%s\n",HC[i]); }
- 译码
void Decoding() { printf("下面对文件CodeFile.data中的代码进行解码形成原文\n结果存入文件Textfile.txt中\n"); FILE *fp4, *fp5; fp4 = fopen("CodeFile.txt", "rb"); fp5 = fopen("Textfile.txt", "w"); char ch; char temp[maxn]; char *s = temp; memset(temp, 0, sizeof(temp)); //while(fgets(temp,maxn*maxn,fp4))//读到换行符,并加上\0,存入temp do { ch = fgetc(fp4); if (ch == '\n') memset(temp, 0, sizeof(temp)), s = temp, fprintf(fp5, "\n"); else if (ch == ' ') memset(temp, 0, sizeof(temp)), s = temp, fprintf(fp5, " "); else { *s++ = ch; if (fun(temp) >= 26&&fun(temp)<52) fprintf(fp5, "%c", 'A' + fun(temp)%26), memset(temp, 0, sizeof(temp)), s = temp; else if(fun(temp)>=0&&fun(temp)<26) fprintf(fp5, "%c", 'a' + fun(temp)), memset(temp, 0, sizeof(temp)), s = temp; else if(fun(temp)>=52&&fun(temp)<62) fprintf(fp5, "%c", '0' + fun(temp)%52), memset(temp, 0, sizeof(temp)), s = temp; else if(fun(temp)==62) fprintf(fp5, "%c", ','), memset(temp, 0, sizeof(temp)), s = temp; else if(fun(temp)==63) fprintf(fp5, "%c", '.'), memset(temp, 0, sizeof(temp)), s = temp; else continue; } } while (ch != EOF); fclose(fp4); fclose(fp5); }
- 打印
void print_tree(HuffmanTree HT,int t,int step)//横向打印哈夫曼树 { int i; if(HT[t].rchild) print_tree(HT, HT[t].rchild, step + 1); for (i = 0; i < step; i++) { printf(" "); fprintf(pt," "); } printf(" -%d-▏\n", HT[t].weight); fprintf(pt," -%d-▏\n",HT[t].weight); if(HT[t].lchild) print_tree(HT, HT[t].lchild, step + 1); } void Output() { FILE *fp6; fp6=fopen("CodePrin.txt","w"); for (int i = 0; i < n; ++i) { printf("%c :%lf\n", ans[i].ch, ans[i].wet*1.0 / sum); } printf("\n******************************************************\n\n"); printf("下面输出输出ToBeTran.data及其报文CodeFile.txt\n\n"); FILE *fp2, *fp3; fp2 = fopen("ToBeTran.txt", "r"); fp3 = fopen("CodeFile.txt", "r"); char ch; printf("ToBeTran.data :\n"); do { ch = fgetc(fp2); printf("%c", ch); } while (ch != EOF); printf("\n\n\n"); printf("CodeFile.txt :\n"); int cnt=0; do { ch = fgetc(fp3); printf("%c", ch); fprintf(fp6,"%c",ch); cnt+=1; if(cnt%50==0) { printf("\n"); fprintf(fp6,"\n"); } } while (ch != EOF); printf("\n\n\n"); printf("\n******************************************************\n\n"); printf("下面输出CodeFile.data及其原文Textfile.txt\n\n"); FILE *fp4, *fp5; fp4 = fopen("CodeFile.txt", "r"); fp5 = fopen("Textfile.txt", "r"); printf("CodeFile.data :\n"); do { ch = fgetc(fp4); printf("%c", ch); } while (ch != EOF); printf("\n\n\n"); printf("Textfile.txt :\n"); do { ch = fgetc(fp5); printf("%c", ch); } while (ch != EOF); printf("\n\n\n"); fclose(fp6); }
-
调试分析
(1)调试过程中遇到的问题- 大小写以及符号的问题。如果只有小写字母,那我们可以根据Ascii码
-'a'
来确定每个字符的相对大小,但是引入了大写和符号,所以我们就需要多增加几个条件分支来保存他们的权值。 - 如何打印树。如果直接选择常规的方法去遍历树就会像得十分的麻烦,所以我选择了横向打印树的方式,类似下图。
(2) 算法的时空复杂度。
假设编码的文本字符串长度为n,字母为k个符号。
由于我们的权值读入是无序的,因此需要先对权值进行排序,时间复杂度O(k*log(k))对于每个编码符号,必须遍历树以解码该符号。树包含k个节点,并且平均需要O(log k)个节点访问来解码符号。所以时间复杂度为O(n log k)。
空间复杂度为树的O(k)和解码文本的O(n)。
- 大小写以及符号的问题。如果只有小写字母,那我们可以根据Ascii码
实验结果:
首先准备好n个权值和一篇英文文章。
I:初始化
67个权值
一篇英语文章
实验终端打印
文件中打印
E:编码
将编码结果打印出来并且保存到CodeFile文件里
文章原文
CodeFile文件
D:译码
将译码之后的结果保存到TextFile文件里
P:打印
将文件CodeFile以紧凑格式显示在终端上,每行50个代码。同时将此字符形式的编码文件写入文件CodePrin中。
CodePrin文件
T:印哈夫曼树
将哈夫曼树打印出来
实验总结
本次实验花费时间比较长,有以下几点感悟:
- 注意内存空间的管理,很有可能就出现内存泄漏的情况。
- 对于文件的打印,最后一定要记得把文件关掉。
- 关于递归问题的文件打印,文件指针最好是要设置成全局变量,不然文件很有可能在中途就关闭。
- 哈夫曼树是一种效率很高的数据结构,如果输入权值前就进行好排序,可能时间复杂度会更好。
- 横向打印树可以是一种输出树结构的好方式。