一、例子引入
相信大家都有这样的经历,当我们给其他人发送比较大一点的文件的时候,我们通常需要先对文件进行压缩,将压缩的文件比如常见的以.zip结尾的文件发送给接收方,接收方再对接收的文件进行解压缩,获取到发送方想要发送的原文件。
至于为什么要这样呢?压缩压缩顾名思义,为了减小发送文件的大小。首先我们应该知道:(1)发送的文件是以二进制流的方式进行通信,所以我们要对文件中的数据进行编码;(2)文件中可能包含多种类型的数据,如文本、图像、音频、视频等。这里我们先不管其他类型的数据,我们只考虑文本类型的数据。假设我们随意的对文本里面的字符A进行编码为01,B字符编码为10,C字符编码为0101,D字符编码为011。这里会出现两个问题:
(1)按照这样的编码方式能够让原文件变成对应的二进制码的时候内存最小吗?(如何编码)
(2)我们能够这样编码吗?(正确性)
对于问题(1),显然不能哈哈,这个是跟文本内容中字符的频率相关的。对于给定的一篇文本,文本中每个字符的频率是确定的,这里我们就让字符在文本中出现的次数来作为它的频率,我们会想对于字符频率大的字符,我们要尽可能让它的编码长度更短;而对于字符频率小的,则可以相对更加的长一点。按照这样编码,肯定可以最节省空间。
对于问题(2),我们也是不能够这样编码的,你可以想一下,生成的二进制数据中的某一段是01100110101,那么我们按照上面给出的编码规则,01对应A,10也能够对应B,011也能够对应D,但是对于0001呢,我们的本意是对应字符C,但是计算机会对应AA,因为它不能够像人一样去思考,所以我们必须给它严格的一一映射,不会让计算机在解释数据时造成歧义,这就要求我们在编码时,一个字符的编码不能是其他所有字符的前缀码,即前缀不重复。
那么下面我们就要考虑如何编码即要让(字符,编码)最节省空间,又要使每个字符的编码不能是其他字符的前缀码。根据要求的这两个性质,我们想到最优二叉树(也叫哈夫曼树)具有这个性质,所以我们下面讲述如何哈夫曼树为什么能满足这两个性质以及如何构建哈夫曼树。
二、构建哈夫曼树
先介绍一点基础知识——二叉树的带权路径长度WPL(Weighted Path Length)。每个叶子节点的深度是指从根节点到该叶子节点的路径长度(经过的边数)。WPL=每个叶子节点的路径长度 * 叶子节点的权值。
下面是一棵最优二叉树:
构建哈夫曼树的步骤:
1、准备数据集,统计每个字符对应的频率,生成字符频率表。
2、根据频率表构建哈夫曼树:
(1)选择两个最小频率的字符节点,申请一个新的节点作为两个选择字符的父节点,构建一棵新的子树,父节点的值就是两个子节点的频率之和,将生成的子树加入到节点集合中去并且把选择的两棵子树删除。
(2)重复操作1,直到节点集合里只剩下一个节点。
这里有个视频网址:https://zhuanlan.zhihu.com/p/144562146
构建哈夫曼树
按照上述构建哈夫曼树的规则,频率小的字符将会在树的较下层,对应路径长度更大;而频率较大的字符在树的上层,对应路径长度小。构建的这样的二叉树WPL会最小,路径长度也就是编码的长度,例如上图中的C节点路径长度为3,对应的二进制编码为010,长度也为3。可以知道,WPL也就等于原文在编码之后的二进制码的长度,WPL最小,二进制码最短,这样就实现了如何编码使压缩后占用内存空间最小。
在生成编码的时候,我们规定从根节点开始,向左走对应0;向右走对应1。由于每个字符都在叶子节点上,不会有那个节点会在从根节点到这个节点的路径上,那么每个字符的编码都不会是其他字符的前缀码,这就保证了编码的正确性。例如上图中字符C的编码,如果B是C的前缀码,那么B必定出现在从根节点到C节点的路径上,那么不是叶子节点,与假设矛盾,所以哈夫曼树保证了前缀不重复这一特点。
至此,我们已经讲完了如何构建哈夫曼树以及使用哈夫曼树来进行编码的合适性与正确性。下面讲哈夫曼树的构建与应用。
三、哈夫曼树的应用——哈夫曼编码与解码
编译环境:Microsoft Visual Studio 2022
编程语言:C语言
流程图:
头文件:
1、抽象数据结构定义Huf_Structure.h
/* Huf_Structure.h */
#ifndef HUF_STRUCTURE_H
#define HUF_STRUCTURE_H
//最长编码长度
#define MAX_CODE_LENGTH 256
typedef struct huffman {
char data;//字符
int freq;//频率
}Huffman;
//哈夫曼节点
typedef struct huf_Node {
Huffman info;
struct huf_Node* l_child;
struct huf_Node* r_child;
char* code;//编码
}Huf_Node;
struct HuffmanTree {
Huf_Node* root;//哈夫曼树根节点
Huffman* totalInfo;//字符频率表
int size;//字符个数
};
#endif
2、实现函数头文件huffman.h:
/* huffman.h */
#include"Huf_Structure.h"
//标准输入输出函数头文件
#include<stdio.h>
//申请堆空间、删除申请空间使用函数的头文件
#include<stdlib.h>
//使用assert函数的头文件,该函数可以判断assert函数括号表达式的真假
#include<assert.h>
//字符串处理的函数
#include<string.h>
#ifndef HUFFMAN_H
#define HUFFMAN_H
//全局变量的声明
//在后面PreTraversal函数有用
int index;
//读入文件大小
int filesize;
//哈夫曼树以及字符频率表和字符个数的结构体声明
struct HuffmanTree* huf;
//编码时候文件读入、写出的指针
FILE* EnfpIn;
FILE* EnfpOut;
//解码时候文件读入、写入的指针
FILE* DefpIn;
FILE* DefpOut;
//数据预处理,并统计huf的size大小
Huffman* statistic(int* count);
//返回一个申请的哈夫曼节点
Huf_Node* creatNewNode(Huffman info);
//申请指向哈夫曼节点的指针数组,返回指针数组的首地址
Huf_Node** fillNodes(struct HuffmanTree* huf);
Huf_Node* buildTree(Huf_Node** nodes, int numNodes);
void generateHuffmanCodes(Huf_Node* root, char* code, int depth);
void PreTraversal(Huf_Node* root, Huf_Node** nodes);
void Encoding(Huf_Node** nodes,int size);
void Decoding(Huf_Node* root);
void freeTree(Huf_Node* root);
#endif
功能函数实现
1、数据预处理
从网上下载一篇英文的文章,移动到该项目同一个文件夹下,打开文件读入指针,从文件中读取内容,并做处理。
//初始化信息,从文件中读取字符以及频率
//传入要压缩的文件,统计字符数以及频率,这里很关键的一个点是由于我们考虑的字符的ASCII码一定在0-255之间,所以
//我们可以用字符转换成int当作下标,而数组的值就是字符的frequency,就是一个映射关系,非常巧妙。
//从文件里读出一个字符我们用到fgetc函数,判断条件为!=EOF这样才比较准确
//还要注意的一个点就是用malloc申请的空间一定要判断是否申请成功,并关闭打开的文件
//返回指向Huffman结构体的指针,即申请Huffman空间的首地址,这样我们就得到了字符对应频率的信息,以及字符个数
Huffman* statistic( int* count) {
int freq[256] = { 0 };
// 统计字符频率
char ch;
while ((ch = fgetc(EnfpIn)) != EOF) {
//EnfpIn为编码读入文件指针
//filesize统计文件字符数量,在后面递归解码时有用
filesize++;
freq[(unsigned char)ch]++;
}
// 统计非零频率的字符数量
*count = 0;
for (int i = 0; i < 256; i++) {
if (freq[i] != 0) {
(*count)++;
}
}
printf("\ncount=%d\n", *count);
// 申请Huffman结构体数组内存
Huffman* huf_info = (Huffman*)malloc(sizeof(Huffman) * (*count));
if (huf_info == NULL) {
perror("Error allocating memory.\n");
return NULL;
}
// 填充Huffman结构体数组
int k = 0;
for (int i = 0; i < 256; i++) {
if (freq[i] != 0) {
huf_info[k].data = (char)i;
huf_info[k].freq = freq[i];
k++;
}
}
return huf_info;
}
2、根据字符频率表创建哈夫曼节点,以及指向这些节点的数组指针
//创建一个新的节点,并初始化信息,字符,字符频率,申请code空间
Huf_Node* creatNewNode(Huffman info) {
Huf_Node* newNode = (Huf_Node*)malloc(sizeof(Huf_Node));
assert(newNode);
newNode->code = (char*)malloc(sizeof(char) * (MAX_CODE_LENGTH+1));
assert(newNode->code);
newNode->info.data = info.data;
newNode->info.freq = info.freq;
newNode->l_child = newNode->r_child = NULL;
return newNode;
}
//创建指向这些节点的指针数组
//下面是根据信息创建这些节点,并且创建一个指针数组来指向这些零散的
//节点,方便我们后面构建哈夫曼树时从所有节点中选出两个最小的节点
//返回指针数组的首地址
Huf_Node** fillNodes(struct HuffmanTree* huf) {
Huf_Node** nodes = (Huf_Node**)malloc(sizeof(Huf_Node*) * huf->size);
for (int i = 0; i < huf->size; i++) {
nodes[i] = creatNewNode(huf->totalInfo[i]);
assert(nodes[i]);
}
return nodes;
}
3、构建哈夫曼树
//创建哈夫曼树
//根据哈夫曼树的构建规则,每个节点都是一棵树,从中选出两个最小节点造一棵新树,并连接这两棵子树,直到剩下最后一棵树
// 参数:nodes,指向那些零散节点的指针数组,用于对节点进行操作;numNodes,挑选节点的范围。
//返回哈夫曼树的根节点指针
//
//下面是一个递归的过程,递归出口,只剩下一棵树;挑选两棵最小的树是关键之处:我们先随便选min1,min2为最小的两个节点,
//并且保证min1<=min2,之后遍历下标从2到numNodes-1的所有节点,根据比较选出两个最小的节点。
//创建一个新节点,传入min1和min2节点的频率之和,字符随便传一个,父节点的左右指针指向这两个节点。
//关键:选完之后我们要保证覆盖掉min1和min2节点,我们就用parent覆盖掉min1,numNodes-1覆盖min2,保证parent一定在指针数组中
Huf_Node* buildTree(Huf_Node** nodes,int numNodes) {
//递归出口
if (numNodes == 1) {
return nodes[0];
}
//确保nodes[min1]是最小的
int min1 = 0, min2 = 1;
if (nodes[min1]->info.freq > nodes[min2]->info.freq) {
int temp = min1;
min1 = min2;
min2 = temp;
}
for (int i = 2; i < numNodes; i++) {
if (nodes[i]->info.freq < nodes[min1]->info.freq) {
min2 = min1;
min1 = i;
}
else if (nodes[i]->info.freq < nodes[min2]->info.freq) {
min2 = i;
}
}
Huffman info = { '\0',nodes[min1]->info.freq + nodes[min2]->info.freq };
Huf_Node* parent = creatNewNode(info);
parent->l_child = nodes[min1];
parent->r_child = nodes[min2];
nodes[min1] = parent;//下一次节点中应该没有nodes[min1],因此要覆盖掉,同时由于parent保存了nodes[min1]和nodes[min2]的地址
//因此并没有影响
nodes[min2] = nodes[numNodes - 1];//在下次遍历的时候由于numNodes-1,所以最后一个节点访问不到,因此将最后一个节点前移
//而且上面也不能交换顺序,考虑刚好是最后一个节点为min2和前一个节点min1,这时显然min2没有改变,但是也已经被访问过
//上述保证parent节点一定还在指针数组中,同时将比较后的两个节点覆盖
return buildTree(nodes, numNodes - 1);
}
这里没有使用最小堆来构建最优二叉树。
4、遍历二叉树让指针数组指向那些二叉树叶子节点
//先序遍历,并且将指针数组指向那些叶子节点
//index:全局变量,方便递归
//由于每个叶子节点分散开,nodes也已经被修改过,因此我们要重新得到指向这些叶子节点的指针数组,通过遍历哈夫曼树,遇到叶子节点
//指针指向该叶子节点
//@return:指向叶子节点的指针数组
void PreTraversal(Huf_Node* root,Huf_Node** nodes) {
if (root == NULL) return;
if (root->l_child == NULL && root->r_child == NULL) {
printf("[%c]-%d %d\n", root->info.data, root->info.data, root->info.freq);
nodes[index++] = root;
}
PreTraversal(root->l_child,nodes);
PreTraversal(root->r_child,nodes);
}
5、生成二进制编码表
//生成哈夫曼编码
//遇到叶子节点将code复制到该节点的code值
//考虑一下可能出现的问题:strcpy一个没有'\0'结尾的字符串能不能行,这样生成哈夫曼编码有没有错误,对于code通过递归
//能否生成正确的编码
void generateHuffmanCodes(Huf_Node* root, char* code, int depth) {
if (root == NULL) {
return;
}
//如果是叶子节点,将code加入
if (root->l_child == NULL && root->r_child == NULL) {
strncpy(root->code, code,depth);
root->code[depth] = '\0';
printf("%c: %s\n", root->info.data, root->code);
}
code[depth] = '0';
generateHuffmanCodes(root->l_child, code, depth + 1);
code[depth] = '1';
generateHuffmanCodes(root->r_child, code, depth + 1);
}
6、对文本进行编码
//遍历文件编码
//从文件中读取一个字符,遍历nodes,根据nodes指向的叶子节点,将对应的code存进目标二进制文件
//size应为字符个数,也是叶子节点个数
void Encoding(Huf_Node** nodes,int size) {
char ch;
while ((ch=fgetc(EnfpIn))!=EOF) {
for (int i = 0; i < size; i++) {
Huf_Node* node = nodes[i];
if (node->info.data == ch) {
fprintf(EnfpOut, "%s", node->code);
}
}
}
}
7、解码同时把内容输入到文件中
每次只会解码一个字符,在外面用while循环,计数为文件大小filesize。
//解码
//根据构建好的哈夫曼树,打开二进制文件,从二进制文件中挨个挨个读取0/1字符,由于一定是前缀码,因此一定会对应一个特定的字符
//按照此种遍历方式,0向左遍历,1向右遍历,遇到叶子节点就将该叶子节点的字符写入目标文件
void Decoding(Huf_Node* root) {
if (root == NULL) {
return;
}
if (root->l_child == NULL && root->r_child == NULL) {
fprintf(DefpOut, "%c", root->info.data);
printf("%c", root->info.data);
return;
}
char byte;
if ((byte = fgetc(DefpIn)) == EOF) { return; }
else {
if (byte == '0') {
Decoding(root->l_child);
}
else if (byte == '1') {
Decoding(root->r_child);
}
else
printf("读码有问题\n");
}
}
8、释放malloc申请的堆空间
void freeTree(Huf_Node* root)
{
if (root == NULL)
{
return;
}
freeTree(root->l_child);
freeTree(root->r_child);
free(root->code);
free(root);
}
整体实现函数huffman.c文件
/* huffman.c */
#define _CRT_SECURE_NO_WARNINGS 1
#include"huffman.h"
//初始化信息,从文件中读取字符以及频率
//传入要压缩的文件,统计字符数以及频率,这里很关键的一个点是由于我们考虑的字符的ASCII码一定在0-255之间,所以
//我们可以用字符转换成int当作下标,而数组的值就是字符的frequency,就是一个映射关系,非常巧妙。
//从文件里读出一个字符我们用到fgetc函数,判断条件为!=EOF这样才比较准确
//还要注意的一个点就是用malloc申请的空间一定要判断是否申请成功,并关闭打开的文件
//返回指向Huffman结构体的指针,即申请Huffman空间的首地址,这样我们就得到了字符对应频率的信息,以及字符个数
Huffman* statistic( int* count) {
int freq[256] = { 0 };
// 统计字符频率
char ch;
while ((ch = fgetc(EnfpIn)) != EOF) {
//filesize统计文件字符数量,在后面递归解码时有用
filesize++;
freq[(unsigned char)ch]++;
}
// 统计非零频率的字符数量
*count = 0;
for (int i = 0; i < 256; i++) {
if (freq[i] != 0) {
(*count)++;
}
}
printf("\ncount=%d\n", *count);
// 申请Huffman结构体数组内存
Huffman* huf_info = (Huffman*)malloc(sizeof(Huffman) * (*count));
if (huf_info == NULL) {
perror("Error allocating memory.\n");
return NULL;
}
// 填充Huffman结构体数组
int k = 0;
for (int i = 0; i < 256; i++) {
if (freq[i] != 0) {
huf_info[k].data = (char)i;
huf_info[k].freq = freq[i];
k++;
}
}
return huf_info;
}
//创建一个新的节点,并初始化信息,字符,字符频率,申请code空间
Huf_Node* creatNewNode(Huffman info) {
Huf_Node* newNode = (Huf_Node*)malloc(sizeof(Huf_Node));
assert(newNode);
newNode->code = (char*)malloc(sizeof(char) * (MAX_CODE_LENGTH+1));
assert(newNode->code);
newNode->info.data = info.data;
newNode->info.freq = info.freq;
newNode->l_child = newNode->r_child = NULL;
return newNode;
}
//创建指向这些节点的指针数组
//下面是根据信息创建这些节点,并且创建一个指针数组来指向这些零散的
//节点,方便我们后面构建哈夫曼树时从所有节点中选出两个最小的节点
//返回指针数组的首地址
Huf_Node** fillNodes(struct HuffmanTree* huf) {
Huf_Node** nodes = (Huf_Node**)malloc(sizeof(Huf_Node*) * huf->size);
for (int i = 0; i < huf->size; i++) {
nodes[i] = creatNewNode(huf->totalInfo[i]);
assert(nodes[i]);
}
return nodes;
}
//创建哈夫曼树
//根据哈夫曼树的构建规则,每个节点都是一棵树,从中选出两个最小节点造一棵新树,并连接这两棵子树,直到剩下最后一棵树
// 参数:nodes,指向那些零散节点的指针数组,用于对节点进行操作;numNodes,挑选节点的范围。
//返回哈夫曼树的根节点指针
//
//下面是一个递归的过程,递归出口,只剩下一棵树;挑选两棵最小的树是关键之处:我们先随便选min1,min2为最小的两个节点,
//并且保证min1<=min2,之后遍历下标从2到numNodes-1的所有节点,根据比较选出两个最小的节点。
//创建一个新节点,传入min1和min2节点的频率之和,字符随便传一个,父节点的左右指针指向这两个节点。
//关键:选完之后我们要保证覆盖掉min1和min2节点,我们就用parent覆盖掉min1,numNodes-1覆盖min2,保证parent一定在指针数组中
Huf_Node* buildTree(Huf_Node** nodes,int numNodes) {
//递归出口
if (numNodes == 1) {
return nodes[0];
}
//确保nodes[min1]是最小的
int min1 = 0, min2 = 1;
if (nodes[min1]->info.freq > nodes[min2]->info.freq) {
int temp = min1;
min1 = min2;
min2 = temp;
}
for (int i = 2; i < numNodes; i++) {
if (nodes[i]->info.freq < nodes[min1]->info.freq) {
min2 = min1;
min1 = i;
}
else if (nodes[i]->info.freq < nodes[min2]->info.freq) {
min2 = i;
}
}
Huffman info = { '\0',nodes[min1]->info.freq + nodes[min2]->info.freq };
Huf_Node* parent = creatNewNode(info);
parent->l_child = nodes[min1];
parent->r_child = nodes[min2];
nodes[min1] = parent;//下一次节点中应该没有nodes[min1],因此要覆盖掉,同时由于parent保存了nodes[min1]和nodes[min2]的地址
//因此并没有影响
nodes[min2] = nodes[numNodes - 1];//在下次遍历的时候由于numNodes-1,所以最后一个节点访问不到,因此将最后一个节点前移
//而且上面也不能交换顺序,考虑刚好是最后一个节点为min2和前一个节点min1,这时显然min2没有改变,但是也已经被访问过
//上述保证parent节点一定还在指针数组中,同时将比较后的两个节点覆盖
return buildTree(nodes, numNodes - 1);
}
//先序遍历,并且将指针数组指向那些叶子节点
//index:全局变量,方便递归
//由于每个叶子节点分散开,nodes也已经被修改过,因此我们要重新得到指向这些叶子节点的指针数组,通过遍历哈夫曼树,遇到叶子节点
//指针指向该叶子节点
//@return:指向叶子节点的指针数组
void PreTraversal(Huf_Node* root,Huf_Node** nodes) {
if (root == NULL) return;
if (root->l_child == NULL && root->r_child == NULL) {
printf("[%c]-%d %d\n", root->info.data, root->info.data, root->info.freq);
nodes[index++] = root;
}
PreTraversal(root->l_child,nodes);
PreTraversal(root->r_child,nodes);
}
//生成哈夫曼编码
//遇到叶子节点将code复制到该节点的code值
//考虑一下可能出现的问题:strcpy一个没有'\0'结尾的字符串能不能行,这样生成哈夫曼编码有没有错误,对于code通过递归
//能否生成正确的编码
void generateHuffmanCodes(Huf_Node* root, char* code, int depth) {
if (root == NULL) {
return;
}
//如果是叶子节点,将code加入
if (root->l_child == NULL && root->r_child == NULL) {
strncpy(root->code, code,depth);
root->code[depth] = '\0';
printf("%c: %s\n", root->info.data, root->code);
}
code[depth] = '0';
generateHuffmanCodes(root->l_child, code, depth + 1);
code[depth] = '1';
generateHuffmanCodes(root->r_child, code, depth + 1);
}
//遍历文件编码
//从文件中读取一个字符,遍历nodes,根据nodes指向的叶子节点,将对应的code存进目标二进制文件
//size应为字符个数,也是叶子节点个数
void Encoding(Huf_Node** nodes,int size) {
char ch;
while ((ch=fgetc(EnfpIn))!=EOF) {
for (int i = 0; i < size; i++) {
Huf_Node* node = nodes[i];
if (node->info.data == ch) {
fprintf(EnfpOut, "%s", node->code);
}
}
}
}
//解码
//根据构建好的哈夫曼树,打开二进制文件,从二进制文件中挨个挨个读取0/1字符,由于一定是前缀码,因此一定会对应一个特定的字符
//按照此种遍历方式,0向左遍历,1向右遍历,遇到叶子节点就将该叶子节点的字符写入目标文件
void Decoding(Huf_Node* root) {
if (root == NULL) {
return;
}
if (root->l_child == NULL && root->r_child == NULL) {
fprintf(DefpOut, "%c", root->info.data);
printf("%c", root->info.data);
return;
}
char byte;
if ((byte = fgetc(DefpIn)) == EOF) { return; }
else {
if (byte == '0') {
Decoding(root->l_child);
}
else if (byte == '1') {
Decoding(root->r_child);
}
else
printf("读码有问题\n");
}
}
void freeTree(Huf_Node* root)
{
if (root == NULL)
{
return;
}
freeTree(root->l_child);
freeTree(root->r_child);
free(root->code);
free(root);
}
逻辑文件Main.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"huffman.h"
int main()
{
//读取文件
char path[256] = ".\\importFile.txt";
//全局变量初始化
index = 0;
filesize = 0;
huf = (struct HuffmanTree*)malloc(sizeof(struct HuffmanTree));
assert(huf);
if ((EnfpIn = fopen(path, "r")) == NULL) {
perror("Error opening file.\n");
}
if ((EnfpOut = fopen("bytes.txt", "w")) == NULL) {
perror("Error opening file.\n");
fclose(EnfpIn);
}
//初始化huf
huf->size = 0;
huf->totalInfo = statistic(&huf->size);
assert(huf->totalInfo);
//重置指针位置
fseek(EnfpIn, 0,SEEK_SET);
Huf_Node** nodes = fillNodes(huf);
huf->root = buildTree(nodes,huf->size);
PreTraversal(huf->root,nodes);
char* code = (char*)malloc(sizeof(char) * MAX_CODE_LENGTH);
generateHuffmanCodes(huf->root, code, 0);
Encoding(nodes,huf->size);
fclose(EnfpIn);
fclose(EnfpOut);
if ((DefpIn = fopen("bytes.txt", "r")) == NULL) {
perror("Error opening file.\n");
}
if ((DefpOut = fopen("output.txt", "w")) == NULL) {
perror("Error opening file.\n");
fclose(DefpIn);
return 1;
}
while (filesize>0)
{
Decoding(huf->root);
filesize--;
}
fclose(DefpIn);
fclose(DefpOut);
//删除二叉树和每个节点的codes
freeTree(huf->root);
//删除保存信息的Huffman节点
free(huf->totalInfo);
//删除构造的指针数组
free(nodes);
free(code);
free(huf);
return 0;
}
整体代码为4个文件huffman.h、Huf_Structure.h、huffman.c和Main.c。
测试结果:
导入原文件:
编码表:
二进码文件:
可以测试几个按照编码表将原文内容转换为二进制码是正确的。
译码文件:
控制台输出:
四、总结
本文详细介绍了哈夫曼编码的原理、构建哈夫曼树的过程以及如何使用哈夫曼树进行文件的压缩和解压缩。以下是总结:
1. 哈夫曼编码是一种基于字符频率的变长编码,它能够将字符映射到二进制序列,使得频率较高的字符编码较短,频率较低的字符编码较长,从而实现压缩。
2. 构建哈夫曼树是哈夫曼编码的核心步骤,通过将频率较低的字符节点合并成一个新的父节点,并重复此过程,最终形成一棵最优二叉树。哈夫曼树具有以下特点:
* 树中每个叶子节点代表一个字符。
* 树中每个非叶子节点的频率等于其左右子节点频率之和。
* 树中任意节点的编码不能是其他节点的编码的前缀。
3. 使用哈夫曼树进行文件压缩和解压缩的步骤如下:
* 统计文件中每个字符的频率,并构建哈夫曼树。
* 根据哈夫曼树生成每个字符的编码。
* 使用编码将文件转换为二进制序列。
* 解压缩时,根据二进制序列和哈夫曼树解码得到原始文件。
4. 本文以C语言为例,实现了哈夫曼编码和解压缩的完整过程,包括数据预处理、构建哈夫曼树、生成编码、编码和解码等步骤。
5. 通过测试,验证了哈夫曼编码和解压缩的正确性,并展示了编码表、二进码文件和译码文件等内容。
6. 哈夫曼编码具有以下优点:
* 压缩效果好,能够有效减少文件大小。
* 解压缩速度快,能够快速恢复原始文件。
* 编码和解码过程简单,易于实现。
总之,哈夫曼编码是一种有效的文件压缩和解压缩方法,具有广泛的应用前景。
本文缺点:
1、不够简洁易懂、注释不够详细。
2、没有分析哈夫曼编码在不同数据集上的表现,了解其对不同数据类型的适应性。
3、没有对哈夫曼编码的性能进行分析,例如压缩比、编码和解码速度。
4、没有深入讨论哈夫曼算法在实际应用中的限制和潜在性优化。
本文主要是介绍如何构建哈夫曼树以及编码、译码的逻辑实现,所以上面这些问题不再分析,有兴趣的读者可以自己查阅资料。
下面是一篇我认为写的非常好的关于哈夫曼编码与解码的文章:
哈夫曼编码及其应用——数据压缩(Huffman compression) - 知乎 (zhihu.com)
上面的部分视频和照片就是来自这篇文章。
还有一个问题,相信某些读者肯定在想怎么去实现哈夫曼树的可视化,这样会让我们在理解哈夫曼树的构建过程更加清晰直接,同时在教学上也有很大用途。
分析一个实现哈夫曼树图形化的文章,有兴趣的读者可以看看: