实验内容
问题描述:
利用哈夫曼编码进行通信可以大大提高信道利用率,缩短信息传输时间,降低传输成本。但是,这要求在发送端通过一个编码系统对待传数据预先编码,在接收端将传来的数据进行译码。对于双工信道(即可以双向传输信息的信道),每端都需要一个完整的编码、译码系统。试为这样的信息收发站写一个哈夫曼码的编码、译码系统。
基本要求:
一个完整的系统应具有以下功能:
(1)初始化。从终端读入字符集大小n,以及n个字符和n个权值,建立哈夫曼树,并将它存在于文件hfmTree中。
(2)编码。利用已建好的哈夫曼树(如不在内存,则从文件hfmTree中读入),对文件的正文进行编码,然后将结果存入文件CodeFile中。
(3)译码。利用已建好的哈夫曼树将文件CodeFile中的代码进行译码,结果存入在文件TextFile中。
测试数据:自定
#define _CRT_SECURE_NO_WARNINGS
#include
#include
//提供许多内存管理函数,如
//malloc(size_t size):动态分配指定大小的内存块,返回指向该内存块的指针。如果分配失败,返回 NULL。
//calloc(size_t num, size_t size):分配足够空间存储 num 个 size 字节的元素,并将这些内存初始化为零,返回指向该内存块的指针。
//realloc(void* ptr, size_t size):重新分配先前分配的内存块的大小。
#include //提供了一系列用于字符串操作的函数
#include //定义了各种数据类型的极限值
// 定义哈夫曼树的节点结构
typedef struct TreeNode {
char ch; // 字符
int weight; // 权重
int parent; // 父节点索引
int lchild; // 左子节点索引
int rchild; // 右子节点索引
} TreeNode;
// 定义哈夫曼树结构
typedef struct HFTree {
TreeNode* data; // 节点数组
int length; // 节点总数
} HFTree;
// 初始化哈夫曼树
HFTree* initTree(int n, char* chars, int* weights) {
HFTree* T = (HFTree*)malloc(sizeof(HFTree));
if (T == NULL) {
printf("内存分配失败\n");
exit(1);
}
T->data = (TreeNode*)malloc(sizeof(TreeNode) * (2 * n - 1)); // 分配内存
if (T->data == NULL) {
printf("内存分配失败\n");
free(T);
exit(1);
}
T->length = 2 * n - 1; // 总节点数
for (int i = 0; i < n; i++) {
T->data[i].ch = chars[i]; // 设置字符
T->data[i].weight = weights[i]; // 设置权重
T->data[i].parent = 0; // 初始时没有父节点
T->data[i].lchild = -1; // 初始时没有左子节点
T->data[i].rchild = -1; // 初始时没有右子节点
}
return T;
}
// 选择两个最小权重的节点
static int* selectMin(HFTree* T, int n) {
int min = INT_MAX;
int secondMin = INT_MAX;
int minIndex = -1;
int secondIndex = -1;
for (int i = 0; i < n; i++) {
if (T->data[i].parent == 0) { // 表示没有父节点
if (T->data[i].weight < min) {
secondMin = min;
secondIndex = minIndex;
min = T->data[i].weight;
minIndex = i;
} else if (T->data[i].weight < secondMin) {
secondMin = T->data[i].weight;
secondIndex = i;
}
}
}
int* res = (int*)malloc(sizeof(int) * 2); // 返回两个最小节点的索引
if (res == NULL) {
printf("内存分配失败\n");
exit(1);
}
res[0] = minIndex;
res[1] = secondIndex;
return res;
}
// 创建哈夫曼树
void createHFTree(HFTree* T, int n) {
for (int i = n; i < T->length; i++) {
int* res = selectMin(T, i);
int min = res[0];
int secondMin = res[1];
T->data[i].ch = '\0'; // 新节点不对应任何字符
T->data[i].weight = T->data[min].weight + T->data[secondMin].weight; // 新节点的权重
T->data[i].lchild = min; // 新节点的左子节点
T->data[i].rchild = secondMin; // 新节点的右子节点
T->data[min].parent = i; // 更新左子节点的父节点
T->data[secondMin].parent = i; // 更新右子节点的父节点
free(res); // 释放内存
}
}
// 保存哈夫曼树到文件
void saveTreeToFile(HFTree* T, const char* filename) {
FILE* file = fopen(filename, "w");
//fopen以写模式打开文件。如果文件不存在则创建新文件;如果文件存在则清空文件内容
if (file == NULL) {
printf("无法打开文件\n");
exit(1);
}
// 输出表格头
fprintf(file, "Huffman Tree List:\n"); //向文件中写入内容
fprintf(file, "Number\tData\tWeight\tParent\tLchild\tRchild\n"); //制表符 \t 分隔各列
//\t 是制表符(tab),用于在输出时插入一个水平制表符,使输出更整齐
for (int i = 0; i < T->length; i++) { //遍历哈夫曼树的所有节点。
char data = T->data[i].ch == '\0' ? '#' : T->data[i].ch; //如节点的字符为空(\0)则用 # 替代
// 输出每个节点的信息
fprintf(file, "%d\t%c\t%d\t%d\t%d\t%d\n",
i, data, T->data[i].weight, T->data[i].parent, T->data[i].lchild, T->data[i].rchild);
}
fclose(file); //fclose(file): 关闭文件,释放与文件关联的资源
}
// 打印哈夫曼树的结构
void output(HFTree* T, int n) {
printf("Huffman Tree List:\n");
printf("Number\tData\tWeight\tParent\tLchild\tRchild\n");
for (int i = 0; i < 2 * n - 1; i++) {
char data = T->data[i].ch == '\0' ? '#' : T->data[i].ch;
printf("%d\t%c\t%d\t%d\t%d\t%d\n", i, data, T->data[i].weight, T->data[i].parent, T->data[i].lchild, T->data[i].rchild);
}
}
// 从文件加载哈夫曼树
HFTree* loadTreeFromFile(const char* filename) {
FILE* file = fopen(filename, "r");
if (file == NULL) {
printf("无法打开文件\n");
exit(1);
}
HFTree* T = (HFTree*)malloc(sizeof(HFTree));
if (T == NULL) {
printf("内存分配失败\n");
exit(1);
}
// 读取节点总数
fscanf(file, "%*s%*s%*s%*s%*s%*s%d", &T->length); // 跳过表头,读取节点总数
// %*s: 忽略匹配的字符串。 %d: 读取一个整数,存储到 T->length 中
//使用 %*s 跳过表头中的每一列名称
// 再读取节点数组
T->data = (TreeNode*)malloc(sizeof(TreeNode) * T->length);
if (T->data == NULL) {
printf("内存分配失败\n");
free(T);
exit(1);
}
for (int i = 0; i < T->length; i++) {
char ch;
fscanf(file, "%*d%*s%c%*s%d%*s%d%*s%d%*s%d",
//使用 %*d 跳过节点编号。 使用 %*s 跳过制表符 \t。
&ch, &T->data[i].weight, &T->data[i].parent, &T->data[i].lchild, &T->data[i].rchild);
T->data[i].ch = (ch == '#') ? '\0' : ch; // 处理 # 符号, 如果是 #,则将其转换为 \0
}
fclose(file);
return T;
}
// 生成哈夫曼编码表
void generateHuffmanCodes(HFTree* T, int* codes, int* lengths, int root, int code, int len) {
if (T->data[root].lchild == -1 && T->data[root].rchild == -1) { // 如果是叶子节点
codes[T->data[root].ch] = code; // 记录编码值
//当前路径的编码值存储到 codes 数组中,索引为当前节点的字符
lengths[T->data[root].ch] = len; // 记录编码长度
return;
}
// 递归生成左子节点编码,code << 1: 将当前路径的编码值左移一位,相当于在编码末尾加个 0,路径的编码长度加一
generateHuffmanCodes(T, codes, lengths, T->data[root].lchild, code << 1, len + 1);
// 递归生成右子节点编码,(code << 1) | 1: 将当前路径的编码值左移一位,然后加上 1,相当于在编码末尾添加一个 1
generateHuffmanCodes(T, codes, lengths, T->data[root].rchild, (code << 1) | 1, len + 1);
}
// 编码文件
void encodeFile(const char* inputFile, const char* outputFile, HFTree* T) {
int* codes = (int*)calloc(256, sizeof(int)); // 初始化编码表
if (codes == NULL) {
printf("内存分配失败\n");
exit(1);
}
int* lengths = (int*)calloc(256, sizeof(int)); // 初始化编码长度表
if (lengths == NULL) {
printf("内存分配失败\n");
free(codes);
exit(1);
}
// 生成编码表
//T->length - 1 是哈夫曼树的根节点索引
generateHuffmanCodes(T, codes, lengths, T->length - 1, 0, 0);
FILE* in = fopen(inputFile, "r"); //以读模式打开输入文件
FILE* out = fopen(outputFile, "w"); //以写模式打开输出文件
if (in == NULL || out == NULL) {
printf("无法打开文件\n");
free(codes);
free(lengths);
exit(1);
}
char ch;
while ((ch = fgetc(in)) != EOF) { // 读取输入文件中的每个字符,直到文件结束(EOF)
for (int i = 0; i < lengths[ch]; i++) { // 输出字符的哈夫曼编码
fprintf(out, "%d", (codes[ch] >> (lengths[ch] - i - 1)) & 1);
// 将当前字符的哈夫曼编码逐位输出到输出文件中
//codes[ch] 是当前字符的编码值,lengths[ch] 是编码长度
//>> 是右移运算符,& 1 是按位与运算符,用于提取最低位
}
}
fclose(in);
fclose(out);
free(codes);
free(lengths);
}
// 译码文件
void decodeFile(const char* inputFile, const char* outputFile, HFTree* T) {
FILE* in = fopen(inputFile, "r"); // 以读模式打开输入文件
FILE* out = fopen(outputFile, "w"); //以写模式打开输出文件
if (in == NULL || out == NULL) {
printf("无法打开文件\n");
exit(1);
}
int root = T->length - 1; // 根节点索引
int current = root; // 当前节点索引
char bit;
while ((bit = fgetc(in)) != EOF) { // 读取编码文件中的每个位
if (bit == '0') {
current = T->data[current].lchild; // 移动到左子节点
} else if (bit == '1') {
current = T->data[current].rchild; // 移动到右子节点
}
if (T->data[current].lchild == -1 && T->data[current].rchild == -1) { // 如果到达叶子节点
fputc(T->data[current].ch, out); // 输出字符
current = root; // 重置到根节点
}
}
fclose(in);
fclose(out);
}
// 释放哈夫曼树
void freeTree(HFTree* T) {
if (T != NULL) {
if (T->data != NULL) {
free(T->data); // 释放节点数组
}
free(T); // 释放哈夫曼树结构
}
}
int main() {
int n;
printf("请输入字符集大小 n: ");
scanf("%d", &n);
char* chars = (char*)malloc(n * sizeof(char)); // 分配字符数组
if (chars == NULL) {
printf("内存分配失败\n");
exit(1);
}
int* weights = (int*)malloc(n * sizeof(int)); // 分配权重数组
if (weights == NULL) {
printf("内存分配失败\n");
free(chars);
exit(1);
}
printf("请输入 %d 个字符和对应的权值:\n", n);
for (int i = 0; i < n; i++) {
printf("字符 %d: ", i + 1);
scanf(" %c", &chars[i]); // 读取字符
printf("权值 %d: ", i + 1);
scanf("%d", &weights[i]); // 读取权重
}
HFTree* T = initTree(n, chars, weights); // 初始化哈夫曼树
createHFTree(T, n); // 创建哈夫曼树
saveTreeToFile(T, "hfmTree.txt"); // 保存哈夫曼树到文件
// 打印哈夫曼树结构
output(T, n);
// 打印哈夫曼编码表
int* codes = (int*)calloc(256, sizeof(int)); // 初始化编码表
if (codes == NULL) {
printf("内存分配失败\n");
exit(1);
}
int* lengths = (int*)calloc(256, sizeof(int)); // 初始化编码长度表
if (lengths == NULL) {
printf("内存分配失败\n");
free(codes);
exit(1);
}
generateHuffmanCodes(T, codes, lengths, T->length - 1, 0, 0); // 生成哈夫曼编码表
//void generateHuffmanCodes(HFTree* T, int* codes, int* lengths, int root, int code, int len)
printf("Huffman Codes:\n");
for (int i = 0; i < n; i++) {
printf("%c: ", chars[i]);
for (int j = 0; j < lengths[chars[i]]; j++) {
printf("%d", (codes[chars[i]] >> (lengths[chars[i]] - j - 1)) & 1);
}
printf("\n");
}
free(codes);
free(lengths);
encodeFile("input.txt", "CodeFile.txt", T); // 编码文件
decodeFile("CodeFile.txt", "TextFile.txt", T); // 译码文件
free(chars); // 释放字符数组
free(weights); // 释放权重数组
freeTree(T); // 释放哈夫曼树
return 0;
}
#include
//提供许多内存管理函数,如
//malloc(size_t size):动态分配指定大小的内存块,返回指向该内存块的指针。如果分配失败,返回 NULL。
//calloc(size_t num, size_t size):分配足够空间存储 num 个 size 字节的元素,并将这些内存初始化为零,返回指向该内存块的指针。
//realloc(void* ptr, size_t size):重新分配先前分配的内存块的大小。
文件操作的基本概念
在 C 语言中,文件操作主要通过标准库
%*s 是 scanf 和 fscanf 等格式化输入函数中的一个特殊格式说明符。它的作用是跳过输入中的匹配项,而不将其存储到变量中。跳过输入中不需要的部分,使得后续的输入能够正确读取。
可以使用 %*s 跳过表头中的每一列名称
\t 是制表符(tab),用于在输出时插入一个水平制表符,使输出更整齐。