本章博文用来使用二叉树的知识来对文件进行压缩与解压缩,这种压缩专门针对 ASCII 码(英文及英文标点)的压缩技术,希望这篇博文能帮助到正在学习或者想要练习二叉树方面知识的同学!!!
开篇我来介绍一下什么是二叉树:
二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”和“右子树”。(示意图如下)
如上图所示,二叉树的“度”小于等于2
那么,什么是哈夫曼压缩呢?这里,我先引进一个知识点——“哈夫曼编码”:
我们为每一种在文件中出现的字符根据其出现频度赋予相应的哈夫曼编码
举个简单的例子,我来提供一个哈夫曼树:
如上图:我们将每次最小的两个节点分别作为“左孩子”和“右孩子”
而存储有效数据的节点我们称之为叶子节点,没有存储有效数据的节点我么称之为根节点(如上图存储58,23,7,3的节点)
在这里,我们令向左为0,向右为1,从根出发,那么,相应的字母及其的哈夫曼编码如下:
f:1
a:01
d:001
e:0000
s:0001
所以,我们进行哈夫曼压缩的步骤其实也就可以总结为如下:
1.统计该文件中出现的不同字节及其频度;
2.根据上述统计数据,构造哈夫曼树;
3.根据上述哈夫曼树,构造每一个字节的哈夫曼编码;
4.将文件中的字节,转换成相应的哈夫曼编码,并将其输出。
所以,在知道了大致的步骤之后,我们就开始编写相应代码:
首先是编写我的博文中几乎每篇都会出现的"mec.h"文件:
#ifndef _MEC_H_
#define _MEC_H_
typedef unsigned char boolean;
typedef boolean u8;
#define TRUE 1
#define FALSE 0
#define SET(v, i) (v |= (1 << ((i) ^ 7)))
#define CLR(v, i) (v &= ~(1 << ((i) ^ 7)))
#define GET(v, i) (((v) & (1 << ((i) ^ 7))) != 0)
#endif
之后是定义一个结构体,它用来存放字符及其相应的频度:
typedef struct FREQ{
unsigned char ch;
int freq;
}FREQ;
那么,接下来的步骤就是编写查找文件中的字符种类以及相应的频度函数:
因为我们要从一个文件中查找,所以,参数一定是文件名;
而我们最终要得到的结果是用结构体数组存放目标结构体,所以,这个函数的返回值就是该结构体数组的指针。
但是,我们还需要在遍历目标文件后返回文件中的字符种类地总数,以便我们之后遍历结构体数组,但是,我们已经确定了返回值,所以,我们就传递该变量的首地址,用指针变量作为参数的形式改变那个变量的值
那么,相应代码段如下:
FREQ *getFreq(const char *fileName, int *chCount) {
FILE *fp;
int ch;
FREQ *freq = NULL;
int bytes[256] = {
0};
int index;
int t = 0;
fp = fopen(fileName, "rb");
ch = fgetc(fp);
while (!feof(fp)) {
++bytes[ch];
ch = fgetc(fp);
}
*chCount = 0;
for (index = 0; index < 256; index++) {
//上两个循环的目的是:计算所有字符种类计算
if (bytes[index] != 0) {
++*chCount;
}
}
freq = (FREQ *) calloc(sizeof(FREQ), *chCount);
for (index = 0; index < 256; index++) {
//这个循环的目的是为储存相应字符信息的结构体(即存储字符种类和频度的结构体)赋值
if (bytes[index] != 0) {
freq[t].ch = index;
freq[t].freq = bytes[index];
t++;
}
}
fclose(fp);
return freq;
}
接下来要构建哈夫曼树的话,就要用一个新的结构体来存放各节点的信息,而哈夫曼树其实也是一种二叉树,所以,我们编写如下结构体:
typedef struct HUFF_TAB {
FREQ freq; //这个成员用于存储该字符的频度以及ASCII码
int leftChild; //若该节点没有左孩子赋值为-1,因为频度不可能是负数
int rightChild; //若该节点没有左孩子赋值为-1,理由如上
boolean isVisited; //这个成员用于表示该节点是否被访问过,用于之后部分函数的部分功能
char *code; //这个成员用于存储该字符的哈夫曼编码
}HUFF_TAB;
那么,结构体我们构建好了,并且查找了该文件中的字符总数和相应频度,按照我们之前列好的步骤,接下来,我们要做的是根据我们之前查找出来的数据,构建哈夫曼树:
现在我们来编写初始化哈夫曼树的函数:
因为我们要得到的结果是表示哈夫曼树的结构体,所以返回值为HUFF_TAB *类型,而根据我们已知,我们可以将叶子结点完善,至于根节点,因为要进行一些操作才能完善,所以,为了代码直观可读性,我们之后编写一个函数专门用来处理根节点。所以,本函数代码如下:
HUFF_TAB *initHuffTab(const FREQ *freq, int *huftabIndex, int alphaCount) {
HUFF_TAB *huffTab = NULL;
int index = 0;
huffTab = (HUFF_TAB *) calloc(sizeof(HUFF_TAB), 2*alphaCount - 1);
for (; index < alphaCount; index++) {
huffTab[index].freq.ch = freq[index].ch;
huffTab[index].freq.freq = freq[index].freq;
huffTab[index].isVisited = FALSE;
huffTab[index].leftChild = huffTab[index].rightChild = -1;
huffTab[index].code = (char *) calloc(sizeof(char), alphaCount);
huftabIndex[freq[index].ch] = index; //这里是用到了之前本人博文《内存对齐模式》中讲过的内存页式管理模式的思想
}
return huffTab;
}
那么,在这里我们编写处理根节点的函数:
同样地,编写前,我们要考虑函数的参数以及返回值:
因为我们要对于结构体数组中查找到的数据进行操作,所以我们要传递的参数为结构体数组类型(即:HUFF_TAB *类型)以及字符种类总数
而我们要得到的结果是给该结构体赋值,所以,这个函数的返回值为void.
那么,相应代码段如下:
void createHuffTree(HUFF_TAB *huffTab, int count) {
int leftChild;
int rightChild; //这里对这两个变量的声明做解释:若直接用scanf函数为结构体成员赋初值,就可能会出现错误,所以我们将要赋的值赋给同类型变量,再将此变量的值赋给该成员
int i; //当然,我们学到现在的程度,看到类似于i,j,k等无特殊意义的函数声明,首先应该想到的是用于循环
int t = count; //因为我们要对于数组进行操作,但是用于循环结束标志的count不能发生改变,所以,设置此变量来同时满足这两个要求
for (i = 0; i < count-1; i++) {
//根据先人的推导,我们发现,在哈夫曼树中,根节点数比叶子节点数少一个
leftChild = findMinFreq(huffTab, count + i); //我们这里用左孩子存当前频度排序中最小的字符的频度(这里用左孩子存最小不是硬性要求)
rightChild = findMinFreq(huffTab, count + i); //这样操作右孩子存的频度一定不会比左孩子村的频度小
huffTab[t].freq.ch = '#'; //这里是我们假设所有“根节点”的ch为字符#
huffTab[t].freq.freq = huffTab[leftChild].freq.freq + huffTab[rightChild].freq.freq;
huffTab[t].leftChild = leftChild;
huffTab[t].rightChild = rightChild;
huffTab[t].isVisited = FALSE;
huffTab[t].code = NULL; //因为我们不知道该字符在哈夫曼树的位置,所以我们先令所有的编码都为0,在后面单独写一个函数为所有存储字符信息的结构体赋值
t++;
}
}
那么,我们接下来编写上个函数中所用到的函数——查找最小频度的孩子节点的频度函数:
int findMinFreq(HUFF_TAB *huffTab, int count) {
int minIndex = -1;
int index;
for (index = 0; index < count; index++) {
if (!huffTab[index].isVisited && (
minIndex == -1 || huffTab[index].freq.freq < huffTab[minIndex].freq.freq)) {
minIndex = index;
}
}
huffTab[minIndex].isVisited = TRUE;
return minIndex;
}
根据我们以往的编程经验,我们编写了生成哈夫曼树的函数,就要立刻想到要去编写销毁哈夫曼树的函数,这样的思考方式可以避免出现很多错误,例如在这个程序中,就可以避免造成内存泄漏。
那么,现在,我们来编写销毁哈夫曼树的函数:
void destoryHuffTab(HUFF_TAB *huffTab, int alphaCount) {
int i;
for (i = 0; i < alphaCount