哈夫曼码编、译码器

实验内容

问题描述:

利用哈夫曼编码进行通信可以大大提高信道利用率,缩短信息传输时间,降低传输成本。但是,这要求在发送端通过一个编码系统对待传数据预先编码,在接收端将传来的数据进行译码。对于双工信道(即可以双向传输信息的信道),每端都需要一个完整的编码、译码系统。试为这样的信息收发站写一个哈夫曼码的编码、译码系统。

基本要求:

一个完整的系统应具有以下功能:

(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 语言中,文件操作主要通过标准库 提供的函数来完成。常见的文件操作函数包括:

  • FILE *fopen(const char *filename, const char *mode): 打开文件。
  • int fclose(FILE *stream): 关闭文件。
  • int fprintf(FILE *stream, const char *format, ...) 和 int fscanf(FILE *stream, const char *format, ...): 格式化输入和输出。
  • int fgetc(FILE *stream): 从文件中读取一个字符。
  • int fputc(int c, FILE *stream): 向文件中写入一个字符。
  • size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream): 从文件中读取数据块。
  • size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream): 向文件中写入数据块。
  •   fopen(filename, "w"): 以写模式打开文件。如果文件不存在,则创建新文件;如果文件存在,则清空文件内容。
  • fprintf(file, "Huffman Tree List:\n"): 向文件中写入内容。
  • fclose(file): 关闭文件,释放与文件关联的资源。
  • fopen(inputFile, "r"): 以读模式打开输入文件。
  • fopen(outputFile, "w"): 以写模式打开输出文件。

%*s 是 scanf 和 fscanf 等格式化输入函数中的一个特殊格式说明符。它的作用是跳过输入中的匹配项,而不将其存储到变量中。跳过输入中不需要的部分,使得后续的输入能够正确读取。

  • 使用 %*d 跳过节点编号。
  • 使用 %*s 跳过制表符 \t。

可以使用 %*s 跳过表头中的每一列名称

\t 是制表符(tab),用于在输出时插入一个水平制表符,使输出更整齐。

  • 哈夫曼树的构建:在构建哈夫曼树的过程中,选择两个最小权重的节点时需要确保这两个节点尚未被其他节点选作子节点(即它们的 parent 属性仍为 0)。此外,新创建的非叶节点不应包含实际的字符信息,通常使用特殊符号(如 '\0' 或 '#')表示。
  • 哈夫曼编码的生成:生成哈夫曼编码时,需要递归地遍历哈夫曼树,直到到达叶节点。在此过程中,需要正确地累积编码值,并记录每个字符的编码长度。编码值通常是从最高有效位向最低有效位累积的,因此在输出编码时需要注意位序。
  • 编码与解码的一致性:为了确保编码与解码的一致性,哈夫曼树在编码和解码过程中必须保持一致。这意味着如果编码时哈夫曼树是从文件加载的,那么解码时也应当从相同的文件加载同样的哈夫曼树。

你可能感兴趣的:(算法,c语言,c#,数据结构,哈夫曼树)