文件压缩与解压:霍夫曼编码
由于计算机的存储空间,文件传输时间成本等条件的限制,产生了对文件进行压缩从而减少文件大小的需求,各种压缩算法及其技术应运而生。其中的霍夫曼编码作为无损压缩当中最好的方法,受到了广泛的应用。
霍夫曼编码(Huffman Coding):
霍夫曼编码是一种无损压缩算法,于1952年由Divid A. Huffman在其博士论文《A Method for the Construction of Minimum-Redundancy Codes》提出。基本原理是利用二进制位代替原先字符进行存储,减少文件计算机存储的总位数从而减少文件的大小。下面用一个实例来说明其原理。
实例:
输入一个文本文件,根据Huffman算法原理对文件进行压缩,将压缩后的所有字符输入到另外一个文件上。
注意:实际要对文件进行压缩需要使用二进制的方式读取文件,构造Huaffman树,接着求出每个字符对应的Huffman编码,得到的编码是二进制是0和1,,而非字符型的0和1。为了说明Huffman编码的原理在本文中我使用的是字符型的0和1(下面会有解释说明)。
Huffman 编码是一种前缀编码,即频率越高的字符对应的编码长度越短。相对于等长度的编码来讲,大多数情况下总编码长度要短的多。对上文给的实例,其实现主要有以下几个步骤:
- 遍历所有字符
遍历文件所有的字符,并统计文件中不相同的字符即其出现的个数。
代码如下:
1 /** 2 读取文本文件,遍历文件中所有的字符,统计每个字符出现的频率,将字符和其对应的频数存储 3 在huffmantable结构体中,并且返回不同字符的个数 4 **/ 5 int readfile(char *filename,huffmantable *node) 6 { 7 8 char temp; 9 int length,i; 10 FILE *file; 11 errno_t err; 12 13 length=0; 14 15 err=fopen_s(&file,filename,"r"); 16 17 if(err!=0) 18 { 19 printf("the file can not open! "); 20 } 21 else 22 { 23 while((temp=fgetc(file))!=EOF) 24 { 25 if(length==0) 26 { 27 node[0].character=temp; 28 node[0].count=1; 29 length++; 30 31 } 32 else 33 { 34 for(i=0;i<length;i++) 35 { 36 if(node[i].character==temp) 37 { 38 39 node[i].count=node[i].count+1;//特别注意node.count没有初始化 40 break; //break:执行break后不执行其后的语句 41 } 42 } 43 if(i==length) 44 { 45 node[length].character=temp; 46 node[length].count=1; 47 length++; 48 } 49 } 50 } 51 } 52 53 fclose(file); 54 return length; 55 }
2. 生产霍夫曼树
对于怎样产生霍夫曼树,引用算法导论的实例。如下图所示:
图片来源:《算法导论》
从该图中可以看到,这是一棵满二叉树,所有的字符信息都存储在叶子节点中,而且出现次数越小的字符越底层,即对应的路径就越长,这和频繁度越高编码越短的原则是一致的。这样总编码长度应该是所有树种最优的。
前缀编码:前缀编码是指任何一个字符的编码都不是其他字符编码的前缀。霍夫曼编码一定是前缀编码的原因:在霍夫曼树种没有一个字符所在的节点是其他字符的父节点。
代码如下:
1 //从所有未加入霍夫曼树中选择频率最小的字符 2 int chooseMin(huffmantable *priTree, int length) 3 { 4 //huffmantable temp; 5 int min=6555334; 6 int minpoint=0; 7 int i; 8 9 for(i=0;i<length;i++) 10 if(priTree[i].count!=-2 && priTree[i].count<min ) 11 { 12 min=priTree[i].count; 13 minpoint=i; 14 } 15 16 return minpoint; 17 18 } 19 20 /* 构建霍夫曼树 */ 21 huffmantable *createHuffmanTree(huffmantable *node, int length) 22 { 23 int i; 24 int m; 25 int Fmin,Smin; 26 int count1; 27 int count2; 28 29 huffmantable *priArray; 30 31 //霍夫曼树中节点总数 32 m=2*length-1; 33 34 //对结构体数组进行扩充 35 priArray=(huffmantable *)malloc(sizeof(huffmantable)*m); 36 37 //复制读取文件后取得信息到新数组 38 for(i=0;i<length;i++) 39 { 40 //memcpy(&priArray[i],&node[i],sizeof(huffmantable)); 41 priArray[i].character=node[i].character; 42 priArray[i].count=node[i].count; 43 priArray[i].lchild=-1; 44 priArray[i].rchild=-1; 45 priArray[i].parent=-1; 46 //priArray 47 } 48 49 //对非叶子节点进行初始化操作 50 for(i=length;i<m;i++) 51 { 52 priArray[i].count=-1; 53 priArray[i].lchild=-1; 54 priArray[i].rchild=-1; 55 priArray[i].parent=-1; 56 } 57 58 59 60 for(i=0;i<length-1;i++) 61 { 62 //选择第一小频率节点, 63 Fmin=chooseMin(priArray,length+i); 64 count1=priArray[Fmin].count; 65 priArray[Fmin].count=-2; 66 67 //选择第二小频率节点 68 Smin=chooseMin(priArray,length+i); 69 count2=priArray[Smin].count; 70 priArray[Smin].count=-2; 71 72 //分别链接到左子节点和右子节点 73 priArray[length+i].lchild=Fmin; 74 priArray[length+i].rchild=Smin; 75 priArray[length+i].count=count1+count2; 76 77 78 79 80 priArray[Fmin].parent=length+i; 81 priArray[Smin].parent=length+i; 82 } 83 84 return priArray; 85 86 }
3. 进行编码
按照生成的霍夫曼树按照左0右1的原则,从根节点开始到每个叶子节点所走的路径形成的二进制序列即为霍夫曼编码。
1 /** 2 对已经构建好了霍夫曼树(以二叉链表形式存储)遍历,生成霍夫曼编码 3 **/ 4 huffmancode *huffmancoding(int length,huffmantable *priarray) 5 { 6 huffmancode *coding; 7 char *temp; 8 int i; 9 int clen; 10 int m; 11 12 coding=(huffmancode *)malloc(sizeof(huffmancode)*length); 13 temp=(char *)malloc(sizeof(char)*length); 14 15 for(i=0;i<length;i++) 16 { 17 coding[i].character=priarray[i].character; 18 } 19 20 21 for(i=0;i<2*length-1;i++) 22 { 23 priarray[i].count=0; 24 } 25 26 m=2*length-2; 27 28 clen=0; 29 30 /* 采用DFS算法进行遍历: 0表示未遍历状态;1表示遍历了左子节点,未遍历右子节点;2 表示遍历了双节点 */ 31 while(m!=-1) 32 { 33 if(priarray[m].count==0) 34 { 35 priarray[m].count=1; 36 if(priarray[m].lchild!=-1) 37 { 38 39 m=priarray[m].lchild; 40 temp[clen++]='0'; 41 } 42 else 43 { 44 coding[m].codeString=(char *)malloc(sizeof(char)*clen); 45 for(i=0;i<clen;i++) 46 { 47 coding[m].codeString[i]=temp[i]; 48 } 49 coding[m].codeString[clen]='