开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
一、实验目的
哈夫曼编码是一种以哈夫曼树(最优二叉树,带权路径长度最小的二叉树)为基础变长编码方法。其基本思想是:将使用次数多的代码转换成长度较短的编码,而使用次数少的采用较长的编码,并且保持编码的唯一可解性。在计算机信息处理中,经常应用于数据压缩。是一种一致性编码法(又称"熵编码法"),用于数据的无损压缩。
要求实现一个完整的哈夫曼编码与译码系统。
二、实验要求
实验要求:
- 从文件中读入任意一篇英文文本文件,分别统计英文文本文件中各字符(包括标点符号和空格)的使用频率;
- 根据已统计的字符使用频率构造哈夫曼编码树,并给出每个字符的哈夫曼编码(字符集的哈夫曼编码表);
- 将文本文件利用哈夫曼树进行编码,存储成压缩文件(哈夫曼编码文件);
- 计算哈夫曼编码文件的压缩率;
- 将哈夫曼编码文件译码为文本文件,并与原文件进行比较。
三、设计思想
程序主要分为编码与译码两部分,思路如下。
图3.1 程序总体思路
相关存储结构及函数定义如下。
表3.1 函数定义
| 定义 | 含义 |
|---|---|
| typedef struct HaffmanNode HaffmanNode, * HaffmanTree; | 字符Haffman结点 |
| typedef struct Dic Dic; | 存放字符及其编码结果的结构体 |
| typedef struct HeapStruct HeapStruct, * Heap; | 堆的结构 |
| HaffmanTree createHaffmanNode(); | 生成Haffman树结点 |
| HaffmanTree createHaffmanTree(); | 构造Haffman树 |
| Heap createHeap(int maxsize); | 创建容量为maxsize的堆 |
| void traverseHeap(Heap H); | 堆的遍历 |
| void insertMinHeap(Heap H, HaffmanTree data); | 最小堆的插入 |
| HaffmanTree deleteMinHeap(Heap H); | 最小堆的删除 |
| int countChFrequency(FILE* fp); | 统计字符出现频率 |
| void genHaffmanCode(HaffmanTree root); | 生成Haffman编码 |
| void encoded(FILE* source, FILE* output); | 编码 |
| void decoded(FILE* source, FILE* output); | 解码 |
1 编码
1.1 生成Haffman编码
对一棵具有n个叶子结点的Haffman树,将树中的每个左分支赋0,右分支赋1,则从根到每个叶子的通路上各个分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。哈夫曼编码是最优前缀编码,能使各种报文对应的二进制串的平均长度最短。
因此,为生成Haffman编码,首先要对文本中每个字符出现的频率进行统计,并利用堆的结构构造了Haffman树,并生成编码。
Haffman树结点的结构定义如下。
图3.2 Haffman树结点的结构题
由于ASCII码值有128个字符,因此创建一个大小为128的Haffman结点指针数组,其下标分别对应各个字符的ASCII码值,并创建全局变量dic作为字典。
图3.3 字典结构
(1)统计字符频率
使用countChFrequency函数进行统计,具体流程如下:
图3.4 统计字符流程题
(2)构造Haffman树
最小堆的根节点为所有元素中最小值,用于构造Haffman树十分方便,因此使用最小堆的存储结构来构造。
构造哈夫曼树的算法步骤如下:
①预处理操作:由所有在文本中出现过的字符节点生成最小堆,并以各字符出现的频率(count)作为其权重。
②依次从最小堆里取出两个结点(孩子结点),并生成一个新的父结点,将其插入最小堆中。其中父结点的count值为两孩子的count值求和。
③重复操作②,直至最小堆中只有一个结点为止。
④取出最小堆中结点,即为Haffman树的根节点root。
(3)生成Haffman编码
下面对各个字符进行编码,编码原则是:从树的根节点开始,进入左子树则编码加0,进入右子树则编码加1,就可以得到对应字符的二进制编码。
图3.5 生成Haffman编码示例
具体编程实现采用从叶子结点逆向遍历至根结点的方法:
即对于每一个叶子结点来说,如果它是父结点的左儿子,则将'0'存入临时字符数组中,如果是右儿子,则存入'1',如此循环直至到达根节点。最后将临时字符数组逆置,便得到字符的编码,将其存入对应结点的code字符数组中。
结果示例(部分):
图3.6 Haffman编码结果部分示例
1.2 文本编码
接下来便是将source文件里的文本按照编码规则以二进制的形式写入到output文件中。
采取以下策略:
将存储内容分为三部分:文件基本信息、正文、字典(Haffman编码表)。结构如图所示,其中每一个小方格代表一个字节。
图3.7 二进制文本存储结构
(1)写入操作
三部分内容写入方式大同小异,以正文内容写入为例。
首先需要一个暂存编码的字符数组tempCode。读取正文内容,不断地将字符对应的编码追加到tempCode的末尾。当发现tempCode的长度大于8位时,将进行写入(逐字节写入)操作。
声明一个char类型字符ch(大小为1个字节),首先将其置为0,即将二进制码置为0000 0000。从tempCode的首字符开始读取8位,如果是'1',则将ch左移一位,然后+1;如果是'0',则仅将ch左移一位。然后将ch写入output文件中即可,同时将tempCode中的内容向前移动8位,如此循环。
图3.8 二进制写入代码示例
当文本读取完毕,发现tempCode中还有不足8位的编码,则在后面补0至8位,然后写入。
同理将文本基本信息以及字典也写入output文件中,最终得到二进制文本,实现了压缩的目的,示例如下。
图3.9 二进制文本示例
2 译码
2.1 读入译码信息
首先需要构造字典,将字符、对应Haffman编码存入。
操作如下:
首先读入文本开头12个字节的3个文件基本信息,依据信息定位至字典存储位置,进行读取。对于每一个字符结点来说,首先读入1个字节,值就是其ASCII码值。然后读取其编码长度,并在编码中读取对应位数,然后转换为字符数组并存入对应结点即可。
如此,便根据文本内存储的基本信息与字典,还原了该文本Haffman编码。
图3.10 字典结构
2.2 文本译码
由于Haffman编码不等长,所以编码的匹配策略对匹配效率影响很大。首先对字典进行按照编码长度升序排序,在之后遍历时便可以以最小代价匹配出正确字符。
采用如下的匹配策略:
首先找出各字符编码中的最长编码数maxLen,声明字符数组codeToBeDecoded存放待译码编码。逐字节将二进制文件中的内容读取到codeToBeDecoded数组中(其中需要进行十进制转二进制字符串,并补0操作)。当发现codeToBeDecoded数组的长度大于maxLen时,将其编码与字典各字符的编码进行逐个内存匹配。匹配成功后,将译码得到的字符写入output文件,并将codeToBeDecoded中编码向前覆盖,如此循环。
当写入的字符数与原文本字符数相同时,则译码完成,此时再往后读取便是字典的信息。
如此,我们便得到了译码后的结果。
图3.11 译码匹配流程图
测试结果
以”瓦尔登湖(英文版).txt”为例,运行结果如下:
图4.1 测试结果
对于一篇59w个字符的小说,压缩为55.65%,压缩耗时0.064秒,译码耗时0.069秒。
四、源码
#define _CRT_SECURE_NO_WARNINGS
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#define MAXSIZE 100
#define MAXBIT 256
/* 字符Haffman结点 */
typedef struct HaffmanNode
{
char character; // 字符
long count; // 出现频数
char code[MAXBIT]; // 编码
struct HaffmanNode *lchild; // 左孩子
struct HaffmanNode *rchild; // 右孩子
struct HaffmanNode *parent; // 父亲
} HaffmanNode, *HaffmanTree;
/* 存放字符及其编码结果的结构体 */
typedef struct Dic
{
HaffmanTree charNode[128]; // 字符结点数组,起到字典的作用(只适用于ASCII码表)
} Dic;
/* 堆的结构 */
typedef struct HeapStruct
{
HaffmanTree *elem; // 存放堆元素(Haffman结点)的数组
int size; // 当前元素个数
int capacity; // 存储容量
} HeapStruct, *Heap;
HaffmanTree createHaffmanNode(); // 生成Haffman树结点
HaffmanTree createHaffmanTree(); // 构造Haffman树
Heap createHeap(int maxsize); // 创建容量为maxsize的堆
void traverseHeap(Heap H); // 堆的遍历
void insertMinHeap(Heap H, HaffmanTree data); // 最小堆的插入
HaffmanTree deleteMinHeap(Heap H); // 最小堆的删除
int countChFrequency(FILE *fp); // 统计字符出现频率
void genHaffmanCode(HaffmanTree root); // 生成Haffman编码
void encoded(FILE *source, FILE *output); // 编码
void decoded(FILE *source, FILE *output); // 解码
Dic dic; // 字典,全局变量,用于编码压缩
int main()
{
float begintime, endtime;
// 编码
FILE *originalText = fopen("./original_text.txt", "rb"); // 源文件
FILE *compressedText = fopen("./compressed_text.txt", "wb"); // 压缩后的二进制文件
if (!(originalText && compressedText))
printf("Failed to open files!\n ");
else
{
begintime = clock(); //计时开始
encoded(originalText, compressedText);
endtime = clock(); //计时结束
printf("Program time consuming: %fs\n\n", (endtime - begintime) / ((double)CLOCKS_PER_SEC));
}
fclose(originalText);
fclose(compressedText);
// 解码
FILE *binText = fopen("./compressed_text.txt", "rb"); // 压缩后的二进制文件
FILE *decodedText = fopen("./decoded_text.txt", "wb"); // 解码后的文件
if (!(binText && decodedText))
printf("Failed to open files!\n");
else
{
begintime = clock(); //计时开始
decoded(binText, decodedText);
endtime = clock(); //计时结束
printf("Program time consuming: %fs\n\n", (endtime - begintime) / ((double)CLOCKS_PER_SEC));
}
fclose(binText);
fclose(decodedText);
system("pause");
return 0;
}
/*---------------------------------------- 编码 ----------------------------------------*/
void encoded(FILE *source, FILE *output)
{
int i, j;
int types = 0;
int textLength = countChFrequency(source); // 计算字符频率
HaffmanTree root = createHaffmanTree(); // 由字符频率构造Haffman树
genHaffmanCode(root); // 编码
// 输出编码结果
printf(" *------------> Huffman Code Table <-----------*\n");
printf(" ____________________________________________\n charcater | frequency | code\n");
printf(" ____________|_____________|_________________\n");
for (i = 0; i < 128; i++)
if (dic.charNode[i]->count > 0)
{
if (dic.charNode[i]->character == 10) // 换行符
printf(" \n\t|%8ld | %s\n", dic.charNode[i]->count, dic.charNode[i]->code);
else if (dic.charNode[i]->character == 0) // 结束符
printf(" \0\t|%8ld | %s\n", dic.charNode[i]->count, dic.charNode[i]->code);
else if (dic.charNode[i]->character == 9) // 制表符
printf(" \t\t|%8ld | %s\n", dic.charNode[i]->count, dic.charNode[i]->code);
else if (dic.charNode[i]->character == 13) // 回车
printf(" \r\t|%8ld | %s\n", dic.charNode[i]->count, dic.charNode[i]->code);
else if (dic.charNode[i]->character == 32) // 空格
printf(" space |%8ld | %s\n", dic.charNode[i]->count, dic.charNode[i]->code);
else
printf(" %c\t|%8ld | %s\n", dic.charNode[i]->character, dic.charNode[i]->count, dic.charNode[i]->code);
types++;
}
printf(" _____________|_____________|_________________\n\n");
// 压缩文件准备操作
fseek(source, 0, SEEK_SET); // 找到源文件开头
fseek(output, 12, SEEK_SET); // 找到输出文件的12个字节后的位置,前12字节存相关信息
int lenCompressedFile = 12; // 压缩文件的长度,单位字节
char tempCode[MAXBIT] = {0}; // 临时存放字符编码
int lenTempCode = 0; // 暂存的字符编码长度
// 写入文本
char ch = fgetc(source);
while (ch != EOF)
{
strcat(tempCode, dic.charNode[ch]->code); // 将字符的编码追加给tempCode暂存
lenTempCode = strlen(tempCode);
ch = 0; //二进制为00000000,作为写入文本的二进制编码的临时容器
while (lenTempCode >= 8) // 暂存编码位数大于8时,执行写入操作
{
for (i = 0; i < 8; i++) // 八位二进制数写入文件一次
{
if (tempCode[i] == '1') // ch的二进制数左移并加1
{
ch = ch << 1;
ch = ch + 1;
}
else
ch = ch << 1; // ch的二进制数左移
}
fwrite(&ch, sizeof(char), 1, output);
lenCompressedFile++;
strcpy(tempCode, tempCode + 8); // temoCode的值重新覆盖
lenTempCode = strlen(tempCode);
}
ch = fgetc(source);
}
if (lenTempCode > 0) // 若最后不满8位,则补0
{
ch = 0;
strcat(tempCode, "00000000");
for (i = 0; i < 8; i++)
{
if (tempCode[i] == '1') // ch的二进制数左移并加1
{
ch = ch << 1;
ch = ch + 1;
}
else
ch = ch << 1; // ch的二进制数左移
}
fwrite(&ch, sizeof(char), 1, output); // 将最后一个字符写入
lenCompressedFile++;
}
// 写入源文件大小、压缩后文件大小和字符种类数(bytes)
fseek(output, 0, SEEK_SET); // 输出文件指针指向开头,写入参数
fwrite(&textLength, sizeof(int), 1, output); // 写入源文件大小
fwrite(&lenCompressedFile, sizeof(int), 1, output); // 写入压缩后文件大小(包括12字节的文本信息及正文,不包括字典)
fwrite(&types, sizeof(int), 1, output); // 写入字符种类数
// 写入字典
fseek(output, lenCompressedFile, SEEK_SET); // 找到output的末尾
HaffmanTree tempNode;
char codeLenBit;
for (i = 0; i < 128; i++)
{
if (dic.charNode[i]->count > 0)
{
tempNode = dic.charNode[i];
lenTempCode = strlen(tempNode->code);
fwrite(&(tempNode->character), 1, 1, output); // 写入字符ASCII码
lenCompressedFile++;
codeLenBit = lenTempCode;
fwrite(&codeLenBit, 1, 1, output); // 写入字符编码的长度-1个字节就够了
lenCompressedFile++;
while (lenTempCode % 8 != 0) // 当编码不够整数字节时,补0
{
strcat(tempNode->code, "0");
lenTempCode = strlen(tempNode->code);
}
while (tempNode->code[0] != 0)
{
ch = 0;
for (j = 0; j < 8; j++)
{
if (tempNode->code[j] == '1')
{
ch = ch << 1;
ch += 1;
}
else
ch = ch << 1;
}
strcpy(tempNode->code, tempNode->code + 8);
fwrite(&ch, 1, 1, output); // 将所得的编码信息写入文件
lenCompressedFile++;
}
}
}
printf("\n>>> Original File Information\n");
printf("- filename: original_text.txt\n- file length: %ld bytes\n", textLength);
printf("\n>>> Compressed File Information\n");
printf("- filename: decoded_text.txt\n- file length: %ld bytes\n", lenCompressedFile);
printf("\nThe compress has finished! Compression ratio: %.2f%%\n", (float)lenCompressedFile / (float)textLength * 100);
}
/*---------------------------------------- 解码 ----------------------------------------*/
void decoded(FILE *source, FILE *output)
{
int i, j, k;
int temp;
unsigned char ch;
int writeCount = 0; // 写入字符数的统计
char tempCode[MAXBIT] = {0}; // 临时存放字符编码
char codeToBeDecoded[MAXBIT] = {0}; // 存放待译码的二进制序列(1个char)
Dic dic_decode; // 解码用字典
// 对dic进行初始化
for (i = 0; i < 128; i++)
{
dic_decode.charNode[i] = (HaffmanTree)malloc(sizeof(HaffmanNode)); // 申请空间
dic_decode.charNode[i]->character = 0; // 此时下标无含义
dic_decode.charNode[i]->count = 0; // 此时用于存放编码长度 strlen(code)
dic_decode.charNode[i]->lchild = NULL;
dic_decode.charNode[i]->rchild = NULL;
dic_decode.charNode[i]->parent = NULL;
dic_decode.charNode[i]->code[0] = 0;
}
// 读取相关文本信息
int textLen, binLen, types, biFileLen, codeLen, byteNum;
fseek(source, 0, SEEK_END);
biFileLen = ftell(source); // 二进制文件总大小
fseek(source, 0, SEEK_SET);
fread(&textLen, sizeof(int), 1, source); // 源文本大小
fread(&binLen, sizeof(int), 1, source); // 压缩后的文件大小(不含字典)
fread(&types, sizeof(int), 1, source); // 字符种类数
// 读取字典并转换成二进制码(char数组)
fseek(source, binLen, SEEK_SET);
for (i = 0; i < types; i++)
{
fread(&dic_decode.charNode[i]->character, 1, 1, source); // 读取字符(ASCII码)
fread(&ch, 1, 1, source);
dic_decode.charNode[i]->count = (long)ch; // 读取字符编码长度 strlen(code)
if (dic_decode.charNode[i]->count % 8 > 0) // 当前字符的编码占了几个字节
byteNum = dic_decode.charNode[i]->count / 8 + 1;
else
byteNum = dic_decode.charNode[i]->count / 8;
for (j = 0; j < byteNum; j++)
{
fread(&ch, 1, 1, source); // 此步读取的是单个字节对应的ASCII码
temp = (int)ch;
_itoa(temp, tempCode, 2); // 将ch的值转为二进制字符串存入tempCode
temp = strlen(tempCode);
for (k = 8; k > temp; k--)
{
strcat(dic_decode.charNode[i]->code, "0"); //位数不足,执行补零操作
}
strcat(dic_decode.charNode[i]->code, tempCode);
}
dic_decode.charNode[i]->code[dic_decode.charNode[i]->count] = 0; // 放上/0
}
// 按Haffman码长度从小到大排序,便于译码时查找 - 冒泡
HaffmanTree tmp;
for (i = 0; i < types; i++)
{
for (j = 0; j < types - i - 1; j++)
{
if (strlen(dic_decode.charNode[j]->code) > strlen(dic_decode.charNode[j + 1]->code))
{
tmp = dic_decode.charNode[j];
dic_decode.charNode[j] = dic_decode.charNode[j + 1];
dic_decode.charNode[j + 1] = tmp;
}
}
}
int maxLen = strlen(dic_decode.charNode[types - 1]->code); // 最长编码数
fseek(source, 12, SEEK_SET); // 指向正文开头
tempCode[0] = 0;
while (1)
{
while (strlen(codeToBeDecoded) < maxLen) // 确保一定找到一个字符
{
fread(&ch, 1, 1, source);
temp = (int)ch;
_itoa(temp, tempCode, 2); // 将ch的值转为二进制字符串存入tempCode
temp = strlen(tempCode);
for (k = 8; k > temp; k--) // 转十进制再转二进制会丢掉0,进行补0操作
strcat(codeToBeDecoded, "0");
strcat(codeToBeDecoded, tempCode);
}
// Haffman码与字符的匹配
for (i = 0; i < types; i++)
{
if (memcmp(dic_decode.charNode[i]->code, codeToBeDecoded, dic_decode.charNode[i]->count) == 0)
break;
}
strcpy(codeToBeDecoded, codeToBeDecoded + dic_decode.charNode[i]->count); // 将已经解译的编码覆盖
ch = dic_decode.charNode[i]->character;
fwrite(&ch, 1, 1, output); //写入解码的文件
writeCount++;
if (writeCount == textLen) // 限定写入只写到正文内容,再往后存储的就是字典
break;
}
printf("The decoded has finish!\n");
}
/*---------------------------------------- 编码所需操作 ----------------------------------------*/
/* 统计字符频率 */
int countChFrequency(FILE *fp)
{
int i;
int length = 0; // 统计文本的长度
// 对dic进行初始化
for (i = 0; i < 128; i++)
{
dic.charNode[i] = (HaffmanTree)malloc(sizeof(HaffmanNode)); // 申请空间
dic.charNode[i]->character = i; // 将下标与ASCII码对应
dic.charNode[i]->count = 0;
dic.charNode[i]->lchild = NULL;
dic.charNode[i]->rchild = NULL;
dic.charNode[i]->parent = NULL;
}
// 对字符进行统计
char ch = fgetc(fp);
while (ch != EOF)
{
dic.charNode[(int)ch]->count++;
ch = fgetc(fp);
length++;
}
return length;
}
/* 生成Haffman编码 */
void genHaffmanCode(HaffmanTree root)
{
int i, j;
int count = 0;
char tempCode[256]; // 临时存放字符编码
HaffmanTree pMove = NULL;
// 生成Haffman编码
for (i = 0; i < 128; i++)
{
if (dic.charNode[i]->count > 0) // 对于出现过的字符,即Haffman树的叶子结点
{
pMove = dic.charNode[i];
// 从叶子逆序到根,将编码逆序存放在tempCode中
while (pMove->parent)
{
if (pMove->parent->lchild == pMove)
tempCode[count] = '0'; // 左子树为0
else
tempCode[count] = '1'; // 右子树为1
count++;
pMove = pMove->parent;
}
// 将tempCode编码逆序存放在字符结点中
for (j = 0; j < count; j++)
dic.charNode[i]->code[j] = tempCode[count - j - 1];
dic.charNode[i]->code[j] = '\0';
count = 0;
}
}
}
/* 生成Haffman树结点 */
HaffmanTree createHaffmanNode()
{
HaffmanTree node = (HaffmanTree)malloc(sizeof(HaffmanNode));
node->character = 0;
node->count = 0;
node->lchild = NULL;
node->rchild = NULL;
node->parent = NULL;
return node;
}
/* 构造Haffman树 */
HaffmanTree createHaffmanTree()
{
int i;
// 由字典构造最小堆
Heap H = createHeap(MAXSIZE);
for (i = 0; i < 128; i++)
{
if (dic.charNode[i]->count > 0)
{
insertMinHeap(H, dic.charNode[i]);
}
}
// 构造Haffman树
while (H->size > 1)
{
// 创建新结点,值为两最小结点的和
HaffmanTree newNode = createHaffmanNode();
HaffmanTree left = deleteMinHeap(H);
HaffmanTree right = deleteMinHeap(H);
newNode->count = left->count + right->count;
newNode->lchild = left;
newNode->rchild = right;
left->parent = newNode;
right->parent = newNode;
// 将新结点插入堆中
insertMinHeap(H, newNode);
}
HaffmanTree root = deleteMinHeap(H);
return root;
}
/* 创建容量为maxsize的堆 */
Heap createHeap(int maxsize)
{
Heap H = (Heap)malloc(sizeof(struct HeapStruct));
H->elem = (HaffmanTree *)malloc(sizeof(HaffmanTree) * (maxsize + 1)); // 从下标为1存放堆元素
H->size = 0;
H->capacity = maxsize;
return H;
}
/* 堆的遍历 */
void traverseHeap(Heap H)
{
int i;
if (H->size == 0)
{
printf("Heap is empty!\n");
return;
}
for (i = 1; i <= H->size; i++)
{
printf("%d ", H->elem[i]->count);
}
printf("\n");
}
/* 最小堆的插入 */
void insertMinHeap(Heap H, HaffmanTree data)
{
int i;
if (H->size == H->capacity) // 堆满
{
printf("Min heap is full!\n");
return;
}
i = H->size + 1; // i指向插入元素后堆中最后一个元素的位置
H->size++;
while (1)
{
if (i <= 1) // 如果堆为空,直接退出去,插入元素
break;
if (!(H->elem[i / 2]->count > data->count)) // 已经找到位置
break;
H->elem[i] = H->elem[i / 2]; // 如果插入位置的父结点大于其值,则将其插入位置与父结点交换
i /= 2;
}
H->elem[i] = data; // 将data插入
}
/* 最小堆的删除 */
HaffmanTree deleteMinHeap(Heap H)
{
int parent, child;
HaffmanTree min, temp;
if (H->size == 0)
{
printf("Heap is empty!\n");
return NULL;
}
min = H->elem[1]; // 取出最小值-根节点
// 将堆最后一个元素作为树根,然后调整树的结构
temp = H->elem[H->size]; // 存放堆的最后一个元素
H->size--;
for (parent = 1; parent * 2 <= H->size; parent = child)
{
child = parent * 2; // 先指向左子结点
// child指向左右子结点的较小者
if ((child != H->size) && (H->elem[child]->count > H->elem[child + 1]->count))
child++;
if (temp->count <= H->elem[child]->count) //找到位置了
break;
else // 将temp移动到下一层
H->elem[parent] = H->elem[child];
}
H->elem[parent] = temp;
return min;
}