文章目录
Ⅰ 前言
在之前的文章里,我讲解了如何将一个字符串逐步生成哈夫曼树最后完成其编码,光说概念对编程是没有任何帮助的,这篇文章会用代码实现哈夫曼编码的构造过程。
这是完成最终项目——哈夫曼压缩&解压缩的第一部分,完成了这个阶段,后面只是改成文件级别的操作就可以了,代码的改动不会太大。
Ⅱ 代码实现哈夫曼编码&解码
按照之前我们讲哈夫曼树的时候,逐步做的一样,我们就按这个顺序来实现代码。
A. 构造哈夫曼树
首先我先给大家看一下这个代码需要的头文件,这是我自己的头文件tyz.h,里面是这个代码需要用到的宏和定义。👇
#ifndef _TYZ_H_
#define _TYZ_H_
#define TRUE 1
#define FALSE 0
#define NOT_FOUND -1
#define SET(value, index) (value |= 1 << (index ^ 7))
#define CLEAR(value, index) (value &= ~(1 << (index ^ 7)))
#define GET(value, index) ((value) & (1 << (index ^ 7)) != 0)
typedef unsigned char boolean;
typedef unsigned int u32;
typedef unsigned short u16;
typedef boolean u8;
int skipBlank(const char *str);
boolean isRealStart(int ch);
#endif
代码里的int我都用u32替代,其他类型也是一样,我都用位数来替代。因为哈夫曼压缩解压缩需要牵扯到将数据压缩成以位为单位的空间中,所以用位数来替代本身的名字更加方便一些。
a. 频度统计
首先,这是我们需要得到的表👇
即输入一个字符串,我们需要根据字符串得到每个字符的频度,并将这个表存储起来,因为之后要根据这个表来构造哈夫曼树。
所以最适合存储字符的数据结构是结构体,这样就可以将一个字符和其频度存储起来,就像一个表一样。以下为我的结构体👇
typedef struct ATTRIBUTE {
u8 character;
u32 frequency;
}ATTRIBUTE;
这就是存储一个字符的结构体,分为字符和频度两个部分。
我们知道,所有的字符都是由ASCII码为基的,ASCII码一共有256个,包括所有的可视字符和所有的不可视字符。所以我的想法是,构造一个大小为256的数组,对应着256个ASCII码,这样一个字符串输入进去,可以根据其ASCII码值分别统计,清晰明了。
这是我主函数里定义的实参,通过传参将其传入函数中。
u8 str[128];
u32 ascii[256] = {
0};
u32 characterCount;
以下为生成频度表的代码👇
ATTRIBUTE *initAttributeList(u8 *str, u32 *ascii, u32 *characterCount) {
u32 i;
u32 index = 0;
u32 count = 0;
ATTRIBUTE *attributeList;
for (i = 0; str[i]; i++) {
ascii[str[i]]++;
}
for (i = 0; i < 256; i++) {
count += (ascii[i] != 0);
}
*characterCount = count;
attributeList = (ATTRIBUTE *) calloc(sizeof(ATTRIBUTE), count);
for (i = 0; i < 256; i++) {
if (ascii[i] != 0) {
attributeList[index].character = (u8) i;
attributeList[index++].frequency = ascii[i];
}
}
return attributeList;
}
str为用户输入的字符串, characterCount为字符种类。因为字符种类在之后的编程中还要用到,所以我直接将它当作一个参数传了进去,这样以后的函数就可以直接使用这个值了。
以上是初始化频度表的内容,既然有初始化,有空间的申请,就必然要有释放。以下为销毁的函数👇
void destoryAttributeList(ATTRIBUTE *attributeList) {
if (NULL == attributeList) {
return;
}
free(attributeList);
}
编写到这一步,我建议先写一个显示的函数,以判断其正确性。👇
void showAttributeList(u32 characterCount, ATTRIBUTE *attributeList) {
u32 i;
for (i = 0; i < characterCount; i++) {
printf("频度:%d 符号:%c\n", attributeList[i].frequency, attributeList[i].character);
}
}
这样,得到频度表便完成了。
b. 生成哈夫曼树
① 初始化节点
一个二叉树的节点由几个信息构成:该节点存储的数据,其左孩子下标,其右孩子下标,其哈夫曼编码。
在后面生成哈夫曼树的过程中,在找最小频度节点时,我需要知道一个节点是否已经参与了构造,所以我除了以上四个信息,还需要知道这个节点是否被访问过。因此我构造的节点的结构体如下👇
typedef struct HUFMAN_TREE_NODE {
boolean visited;
u8 *hufmanCode;
u32 leftChild;
u32 rightChild;
ATTRIBUTE attribute;
}HUFMAN_TREE_NODE;
所以在生成哈夫曼树之前,我们需要对结构体进行初始化,即对每个结点的初始化。
以下为初始化节点的函数👇
HUFMAN_TREE_NODE *initHufmanTreeNode(u32 characterCount, u32 *orientate, ATTRIBUTE *attributeList) {
u32 i;
u32 nodeCount;
HUFMAN_TREE_NODE *hufmanTreeNode;
nodeCount = characterCount * 2 - 1;
hufmanTreeNode = (HUFMAN_TREE_NODE *) calloc(sizeof(HUFMAN_TREE_NODE), nodeCount);
for (i = 0; i < characterCount; i++) {
hufmanTreeNode[i].visited = FALSE;
hufmanTreeNode[i].hufmanCode = (u8 *) calloc(sizeof(u8), characterCount);
hufmanTreeNode[i].leftChild = hufmanTreeNode[i].rightChild = -1;
hufmanTreeNode[i].attribute = attributeList[i];
orientate[attributeList[i].character] = i;
}
return hufmanTreeNode;
}
根据之前的公式,我们有characterCount个字符,所以生成的节点数有 characterCount * 2 - 1 个。
有一行代码需要注意,在每次循环的末尾。
orientate[attributeList[i].character] = i;
orientate是我在主函数定义的另一个数组。其意为确定方位。我们将一个符号的ASCII码为下标,将其节点的顺序i作为值存储在这个数组中,之后在编码中,我们就可以直接根据一个符号的ASCII码将其信息提取出来了。
u32 orientate[256] = {
0};
有了初始化,还是和上一个一样,我们需要一个用来释放空间的销毁函数和一个用来判断对错的显示函数。👇
void showHufmanTreeNode(u32 characterCount, HUFMAN_TREE_NODE *hufmanTreeNode) {
u32 i;
printf("字符 频度 左孩子 右孩子 编码\n");
for (i = 0; i < characterCount; i++) {
printf("%-5c %-5d %-7d %-7d %-10s\n",
hufmanTreeNode[i].attribute.character,
hufmanTreeNode