昨天本来就把这篇文章发出来了,但是程序有一点小的问题,而且没有解码步骤,几天全部补上。
霍夫曼编码是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;
}