昨天本来就把这篇文章发出来了,但是程序有一点小的问题,而且没有解码步骤,几天全部补上。
霍夫曼编码是Huffman在MIT的博士毕业论文中提出的一种编码方法。因为它的简单实用,所以虽然已经过去了很多很多年,但这种方法依然经久不衰。说来惭愧,虽说是通信专业科班出身,但是信息论的内容已经忘得差不多了,只记住了如何编码,而这种编码背后所蕴含的复杂的数学推导(否则也不能作为博士论文发表啊),已经几乎没有印象了。
通俗的说,就是假如我们知道我们要发的消息一定是由a,b,c,d,e,f,g,h8个字母组成的。而且每个字符出现的概率是确定的。比如a出现概率有60%,b有20%等等,它的基本思想是,对于出现频率高的字符,用较少的比特表示,而出现频率低的字符,则用较长的比特表示。这样与直接从000到111的平局分配相比,当字符很多的时候,可以减少总的比特数。
它的编码过程如下:
假设N个字符构成了集合h
1.从h中选择2个概率最小的字符x,y,它们的权值分别为wx,wy。
2.用x,y构造二叉树X。X的左右节点为x,y,这个节点的权值为wx+wy
3.将新产生的二叉树X加入到集合h中,同时将x,y删去
4.不断重复1~3,直到h中只剩下一个元素。
5.沿着产生的树,一边为0,另一边为1,直到叶子节点,则每个叶子节点经过的0、1路径就是该叶子节点对应字符的编码。
让我们先看看定义的数据结构:
//26个英文字母 #define SIZE 26 typedef struct Honde { char val; double p;//概率值 int parent; int LeftChild; int RightChild; }Hnode,*pHnode; //数组用来承载集合h Hnode H[400];
程序中有一点小的技巧,就是树的父亲、孩子指针不再是指向另一棵树,而是存放这棵树在树类型的数组H中的下标。
让我们先看看主函数:
#include "huffman.h" int main() { if(!initData()) return -1; int hsize = HuffmanCode(SIZE); decode(hsize); return 0; }
可以看出,整个程序分为3个步骤:初始化数据,编码,以及解码。
初始化数据的任务,就是读取一份文件,对立面的字数进行统计,并求出对应的概率。这里为了简单,只统计了小写字母。
//读入文本,并统计每个字符的概率 bool initData() { FILE *fp; //注意:如果使用\表示下一级目录,则会当做转义字符 fp = fopen("D:\\MyPrograms\\ds\\mytext.txt","r"); if(NULL == fp) { printf("can't open the file!"); return false; } //记录文件中每个字母的个数,charCount[0]记录的是总数 int charCount[SIZE+1]; for(int i = 0; i < SIZE+1;++i) { charCount[i] = 0; } //字符数组用来标识每个字符 char firstChar = 'a'; char word[SIZE]; for(int i = 0; i < SIZE;++i,++firstChar) word[i] = firstChar; char ch; while((ch = getc(fp)) != EOF) { ++charCount[0]; switch(ch) { case 'a': ++charCount[1]; break; case 'b': ++charCount[2]; break; case 'c': ++charCount[3]; break; case 'd': ++charCount[4]; break; case 'e': ++charCount[5]; break; case 'f': ++charCount[6]; break; case 'g': ++charCount[7]; break; case 'h': ++charCount[8]; break; case 'i': ++charCount[9]; break; case 'j': ++charCount[10]; break; case 'k': ++charCount[11]; break; case 'l': ++charCount[12]; break; case 'm': ++charCount[13]; break; case 'n': ++charCount[14]; break; case 'o': ++charCount[15]; break; case 'p': ++charCount[16]; break; case 'q': ++charCount[17]; break; case 'r': ++charCount[18]; break; case 's': ++charCount[19]; break; case 't': ++charCount[20]; break; case 'u': ++charCount[21]; break; case 'v': ++charCount[22]; break; case 'w': ++charCount[23]; break; case 'x': ++charCount[24]; break; case 'y': ++charCount[25]; break; case 'z': ++charCount[26]; break; default: break; } } printf("总单词个数为%d:\n",charCount[0]); for(int i = 0;i < SIZE; ++i) printf("字符%c: %d\t",word[i],charCount[i+1]); printf("\n每组字符的概率为:\n"); //记录概率的数组 double p[SIZE]; for(int i = 0; i < SIZE;++i) { p[i] = (double)charCount[i+1] / (double)charCount[0]; printf("字符%c:%f\t",word[i],p[i]); } printf("\n"); fclose(fp); //初始化H矩阵 for(int i = 0; i < SIZE;++i) { H[i].val = word[i]; H[i].p = p[i]; H[i].LeftChild = H[i].RightChild = H[i].parent = -1; } return true; }
然后再看编码步骤:
int HuffmanCode(int n) { //合并后的新结构体依次放在后面H数组的后面 int cnt = n; //直到合并的剩了最后一个元素,停止合并 while(cnt > 1) { int i = 0,j = 0; select2MinValue(n,&i,&j); // printf("\ni = %d,j = %d\n",i,j); GenerrateBineryTree(n,i,j,n); ++n; --cnt; } outputCode(n); //返回值为整个数组的总的元素个数 return n; }
可以看出编码步骤基本对应于我们前面提到的步骤:先选择两个概率最小的节点,然后把它们合并成新的节点,放在数组的末尾。最后遍历树来实现整个编码。唯一有一点小的技巧的地方在于:在选择最小概率的节点时,需要判断这个节点是否已经被选择过了,这通过合并节点时记录子节点的父节点可以判断,这样就少去了原步骤中的删除x,y。
//遍历数组:找出其中最小的元素,并将其通过index和inex传出去 void select2MinValue(int n,int *minIndex,int *subminIndex) { double min = 1; double submin = 1; for(int i = 0; i < n;++i) { if(H[i].parent != -1) continue; if(H[i].p <= min) { submin = min; min = H[i].p; *subminIndex = *minIndex; *minIndex = i; } else { if(H[i].p < submin) { submin = H[i].p; *subminIndex = i; } } } } void GenerrateBineryTree(int n,int index1,int index2,int newindex) { H[newindex].LeftChild = index1; H[newindex].RightChild = index2; H[newindex].parent = -1; H[newindex].p = H[index1].p + H[index2].p; H[index1].parent = newindex; H[index2].parent = newindex; } int outputCode(int n) { //先输出整个数组的情况: for(int i = 0; i < n;++i) { printf("序号:%d\t概率:%f\t父亲:%d\tleft:%d\tright%d\n",i,H[i].p,H[i].parent,H[i].LeftChild,H[i].RightChild); } //密码表 FILE *code = fopen("codelist.txt","w"); if(NULL == code ) return -1; //通过一个动态字符串数组来存储这个元素的编码 char** elemnetCode = (char**)malloc(sizeof(char*)*SIZE); //对于实际存在的元素 for(int i = 0; i < SIZE;++i) { //遍历每个元素的路径,通过遍历来确定每条路径的长度 int currentIndex = i; int fatherIndex = H[i].parent; int cnt = 1; while(H[currentIndex].parent != -1) { currentIndex = fatherIndex; fatherIndex = H[fatherIndex].parent; ++cnt; } int charsize = cnt; //为每个元素分配合适的存储空间 elemnetCode[i] = (char*)malloc(sizeof(char)*(charsize)); //重新遍历一次,这次遍历的任务是给节点赋值 currentIndex = i; fatherIndex = H[i].parent; int k = cnt; elemnetCode[i][--k] = NULL; while(H[currentIndex].parent != -1) { if(H[fatherIndex].LeftChild == currentIndex) elemnetCode[i][--k] = '0'; else elemnetCode[i][--k] = '1'; currentIndex = fatherIndex; fatherIndex = H[fatherIndex].parent; } printf("第%d个元素编码为%s\n",i,elemnetCode[i]); fprintf(code,"%c: %s\n",H[i].val,elemnetCode[i]); } fclose(code); //打开待加密的文件 FILE *fp = fopen("D:\\MyPrograms\\ds\\mytext.txt","r"); if(NULL == fp) { printf("can't open sourse file"); return -1; } FILE *Ciphertext = fopen("Ciphertext.txt","w+"); if(NULL == Ciphertext) { printf("can't open Ciphertext file"); return -1; } char letter; while(( letter = fgetc(fp))!= EOF) { char first = 'a'; int number = 0; while( first != letter) { ++first; ++number; } //加一行打印 // printf("%s\n",elemnetCode[number]); fprintf(Ciphertext,"%s",elemnetCode[number]); } fclose(fp); fclose(Ciphertext); //释放内存 for(int i = 0 ;i < SIZE;++i) free(elemnetCode[i]); free(elemnetCode); return 0; }
最后再看解码,解码的原理比较比较简单,由于霍夫曼码是“非前缀码”,所以只需要从根开始,按照编码指定的方向沿着树走,走到最后就找到了对应的元素了。
//解码 bool decode(int hsize) { FILE *fp = fopen("Ciphertext.txt","r"); if(NULL == fp) { printf("can't find Ciphertext"); return false; } char letter; int i = 6000; char *content = (char*)malloc(sizeof(char)* i); //计算整个编码有多少位 i = 0; int cnt = 0; while(( letter = getc(fp))!= EOF) { content[i] = letter; ++i; ++cnt; } content[i] = NULL; int index = hsize-1; for(int j = 0; j < cnt;++j) { if('0' == content[j]) index = H[index].LeftChild; else index = H[index].RightChild; if(H[index].LeftChild == -1 && H[index].RightChild == -1) { printf("%c",H[index].val); index = hsize-1; } } free(content); return true; }