Huffman树C语言如何实现编码与解码?

99ANYc3cd6
预计阅读时长 43 分钟
位置: 首页 C语言 正文

Huffman 编码是一种非常经典的无损数据压缩算法,其核心思想是为出现频率高的字符分配较短的编码,为出现频率低的字符分配较长的编码,从而使得整个编码后的字符串长度最短。

huffmantree c语言
(图片来源网络,侵删)

实现 Huffman 编码通常需要以下几个步骤:

  1. 统计频率:统计原始数据中每个字符出现的频率。
  2. 构建 Huffman 树:根据频率构建一棵最优二叉树。
  3. 生成编码表:遍历 Huffman 树,为每个字符生成其对应的二进制编码。
  4. 编码:根据编码表,将原始数据转换为二进制编码流。
  5. 解码:根据 Huffman 树,将二进制编码流还原为原始数据。

下面我们分步用 C 语言实现这个过程。


数据结构定义

我们需要定义几种核心数据结构。

a) Huffman 树节点

Huffman 树的每个节点都是一个结构体,它包含:

huffmantree c语言
(图片来源网络,侵删)
  • char data: 存储的字符(对于叶子节点)。
  • int freq: 该字符(或子树)的频率。
  • unsigned int code: 存储该字符的 Huffman 编码(一个整数,用其二进制位表示编码)。
  • int code_len: 编码的长度(即有多少位)。
  • left, right: 指向左右子树的指针。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Huffman 树节点结构
typedef struct HuffmanNode {
    char data;              // 字符
    int freq;               // 频率
    unsigned int code;      // 编码 (用整数的二进制位表示)
    int code_len;           // 编码长度
    struct HuffmanNode *left;
    struct HuffmanNode *right;
} HuffmanNode;

b) 最小优先队列(用于构建树)

构建 Huffman 树需要反复选择频率最小的两个节点,使用最小优先队列(或最小堆)可以高效地完成这个操作,为了简化,我们这里用有序链表来模拟最小优先队列。

// 最小优先队列(用有序链表实现)
typedef struct MinPriorityQueue {
    HuffmanNode *head;
} MinPriorityQueue;
// 创建一个空的优先队列
MinPriorityQueue* createQueue() {
    MinPriorityQueue* queue = (MinPriorityQueue*)malloc(sizeof(MinPriorityQueue));
    queue->head = NULL;
    return queue;
}

辅助函数

a) 创建新节点

// 创建一个新的 Huffman 节点
HuffmanNode* createNode(char data, int freq) {
    HuffmanNode* node = (HuffmanNode*)malloc(sizeof(HuffmanNode));
    node->data = data;
    node->freq = freq;
    node->code = 0;
    node->code_len = 0;
    node->left = node->right = NULL;
    return node;
}

b) 向优先队列中插入节点(并保持有序)

// 向优先队列中插入一个节点,并保持链表按频率升序排列
void insertQueue(MinPriorityQueue* queue, HuffmanNode* node) {
    // 如果队列为空,或者新节点的频率小于头节点
    if (queue->head == NULL || node->freq < queue->head->freq) {
        node->right = queue->head;
        queue->head = node;
    } else {
        // 否则,找到合适的插入位置
        HuffmanNode* current = queue->head;
        while (current->right != NULL && current->right->freq <= node->freq) {
            current = current->right;
        }
        node->right = current->right;
        current->right = node;
    }
}

c) 从优先队列中取出最小节点

// 从优先队列中取出频率最小的节点
HuffmanNode* extractMin(MinPriorityQueue* queue) {
    if (queue->head == NULL) {
        return NULL;
    }
    HuffmanNode* minNode = queue->head;
    queue->head = queue->head->right;
    minNode->right = NULL; // 断开连接
    return minNode;
}

构建 Huffman 树

这是算法的核心,我们不断从优先队列中取出两个最小频率的节点,创建一个新节点作为它们的父节点,然后将这两个节点和新节点重新放回队列,直到队列中只剩下一个节点,这个节点就是 Huffman 树的根。

// 构建 Huffman 树
HuffmanNode* buildHuffmanTree(char data[], int freq[], int size) {
    MinPriorityQueue* queue = createQueue();
    // 1. 为每个字符创建一个节点并加入优先队列
    for (int i = 0; i < size; i++) {
        insertQueue(queue, createNode(data[i], freq[i]));
    }
    // 2. 循环直到队列中只剩一个节点
    while (queue->head->right != NULL) {
        // 取出两个最小频率的节点
        HuffmanNode* left = extractMin(queue);
        HuffmanNode* right = extractMin(queue);
        // 创建一个新的内部节点,频率为子节点频率之和
        // 内部节点的数据设为 '\0',表示它不是叶子节点
        int sum_freq = left->freq + right->freq;
        HuffmanNode* newNode = createNode('\0', sum_freq);
        newNode->left = left;
        newNode->right = right;
        // 将新节点加入队列
        insertQueue(queue, newNode);
    }
    // 3. 队列中剩下的最后一个节点就是根节点
    HuffmanNode* root = extractMin(queue);
    free(queue); // 释放队列结构本身
    return root;
}

生成 Huffman 编码表

通过遍历 Huffman 树,我们可以为每个叶子节点(即字符)生成其编码,我们使用深度优先搜索(DFS),在遍历时记录路径:向左走记 0,向右走记 1

// 用于存储字符和对应编码的结构体
typedef struct CodeTable {
    char data;
    unsigned int code;
    int code_len;
} CodeTable;
// 递归遍历 Huffman 树以生成编码表
void generateCodes(HuffmanNode* root, CodeTable table[], int *index, unsigned int code, int code_len) {
    if (root == NULL) {
        return;
    }
    // 如果是叶子节点,则保存编码
    if (root->left == NULL && root->right == NULL) {
        table[*index].data = root->data;
        table[*index].code = code;
        table[*index].code_len = code_len;
        (*index)++;
        return;
    }
    // 向左,编码加 0
    generateCodes(root->left, table, index, (code << 1) | 0, code_len + 1);
    // 向右,编码加 1
    generateCodes(root->right, table, index, (code << 1) | 1, code_len + 1);
}
// 生成编码表的包装函数
CodeTable* createCodeTable(HuffmanNode* root, int unique_char_count) {
    CodeTable* table = (CodeTable*)malloc(unique_char_count * sizeof(CodeTable));
    int index = 0;
    generateCodes(root, table, &index, 0, 0);
    return table;
}

编码与解码

a) 编码

将输入字符串转换为二进制编码流,为了方便,我们将每一位编码写入一个文件(encoded.bin),其中前半部分是编码表,后半部分是数据。

huffmantree c语言
(图片来源网络,侵删)
// 打印编码表(用于调试)
void printCodeTable(CodeTable table[], int size) {
    printf("Character | Code (Binary)\n");
    printf("-------------------------\n");
    for (int i = 0; i < size; i++) {
        // 打印字符
        if (table[i].data == ' ') {
            printf("  space   | ");
        } else if (table[i].data == '\n') {
            printf("   \\n    | ");
        } else {
            printf("    %c    | ", table[i].data);
        }
        // 打印二进制编码
        for (int j = table[i].code_len - 1; j >= 0; j--) {
            printf("%d", (table[i].code >> j) & 1);
        }
        printf("\n");
    }
}
// 编码函数
void encodeString(const char* input, HuffmanNode* root) {
    // 1. 创建编码表
    // 首先需要知道有多少个唯一字符,这里简化处理,假设字符集是ASCII
    int freq[256] = {0};
    for (int i = 0; input[i] != '\0'; i++) {
        freq[(unsigned char)input[i]]++;
    }
    int unique_char_count = 0;
    char data[256];
    int f[256];
    for (int i = 0; i < 256; i++) {
        if (freq[i] > 0) {
            data[unique_char_count] = (char)i;
            f[unique_char_count] = freq[i];
            unique_char_count++;
        }
    }
    HuffmanNode* huffmanRoot = buildHuffmanTree(data, f, unique_char_count);
    CodeTable* codeTable = createCodeTable(huffmanRoot, unique_char_count);
    // 2. 打印编码表
    printf("\nHuffman Code Table:\n");
    printCodeTable(codeTable, unique_char_count);
    // 3. 将编码写入文件
    FILE *file = fopen("encoded.bin", "wb");
    if (!file) {
        perror("Failed to open file for writing");
        return;
    }
    // 写入编码表信息:唯一字符数量
    fwrite(&unique_char_count, sizeof(int), 1, file);
    // 写入编码表
    for (int i = 0; i < unique_char_count; i++) {
        fwrite(&codeTable[i].data, sizeof(char), 1, file);
        fwrite(&codeTable[i].code, sizeof(unsigned int), 1, file);
        fwrite(&codeTable[i].code_len, sizeof(int), 1, file);
    }
    // 4. 编码输入字符串并写入文件
    unsigned long long encoded_data = 0;
    int encoded_bits = 0;
    for (int i = 0; input[i] != '\0'; i++) {
        char c = input[i];
        CodeTable* ct = codeTable;
        int found = 0;
        for (int j = 0; j < unique_char_count; j++) {
            if (ct[j].data == c) {
                // 将编码移位到正确位置
                encoded_data = (encoded_data << ct[j].code_len) | ct[j].code;
                encoded_bits += ct[j].code_len;
                found = 1;
                break;
            }
        }
        if (!found) {
            printf("Error: Character '%c' not found in code table.\n", c);
        }
    }
    // 写入编码后的位数
    fwrite(&encoded_bits, sizeof(int), 1, file);
    // 写入编码后的数据(按字节写入)
    int bytes_to_write = (encoded_bits + 7) / 8;
    fwrite(&encoded_data, sizeof(unsigned char), bytes_to_write, file);
    fclose(file);
    printf("\nEncoding complete. Data saved to 'encoded.bin'.\n");
}

b) 解码

从文件中读取编码表和编码数据,然后根据 Huffman 树将二进制编码流还原为原始字符串。

// 解码函数
void decodeFile(const char* filename) {
    FILE *file = fopen(filename, "rb");
    if (!file) {
        perror("Failed to open file for reading");
        return;
    }
    // 1. 读取编码表
    int unique_char_count;
    fread(&unique_char_count, sizeof(int), 1, file);
    CodeTable* codeTable = (CodeTable*)malloc(unique_char_count * sizeof(CodeTable));
    for (int i = 0; i < unique_char_count; i++) {
        fread(&codeTable[i].data, sizeof(char), 1, file);
        fread(&codeTable[i].code, sizeof(unsigned int), 1, file);
        fread(&codeTable[i].code_len, sizeof(int), 1, file);
    }
    // 2. 读取编码后的位数和数据
    int encoded_bits;
    fread(&encoded_bits, sizeof(int), 1, file);
    int bytes_to_read = (encoded_bits + 7) / 8;
    unsigned char* encoded_buffer = (unsigned char*)malloc(bytes_to_read);
    fread(encoded_buffer, sizeof(unsigned char), bytes_to_read, file);
    fclose(file);
    // 3. 重建 Huffman 树
    char data[256];
    int freq[256];
    for (int i = 0; i < unique_char_count; i++) {
        data[i] = codeTable[i].data;
        freq[i] = 0; // 重建时频率不重要,我们只需要树的结构
        // 为了简单,我们假设频率为1
        freq[i] = 1; 
    }
    HuffmanNode* root = buildHuffmanTree(data, freq, unique_char_count);
    // 4. 根据编码数据遍历树进行解码
    printf("\nDecoded String:\n");
    HuffmanNode* current = root;
    int bit_pos = 0;
    int total_bits_read = 0;
    while (total_bits_read < encoded_bits) {
        unsigned char byte = encoded_buffer[bit_pos / 8];
        int bit = (byte >> (7 - (bit_pos % 8))) & 1;
        if (bit == 0) {
            current = current->left;
        } else {
            current = current->right;
        }
        // 如果到达叶子节点
        if (current->left == NULL && current->right == NULL) {
            printf("%c", current->data);
            current = root; // 回到根节点
        }
        total_bits_read++;
        bit_pos++;
    }
    printf("\n");
    // 释放内存
    free(encoded_buffer);
    free(codeTable);
    // 注意:这里应该有一个函数来释放整棵Huffman树,为了简化省略了
}

主函数与完整示例

将所有部分组合起来,并提供一个 main 函数来驱动整个流程。

// 用于释放 Huffman 树的内存(重要!)
void freeHuffmanTree(HuffmanNode* root) {
    if (root == NULL) {
        return;
    }
    freeHuffmanTree(root->left);
    freeHuffmanTree(root->right);
    free(root);
}
int main() {
    char input_string[] = "this is an example for huffman encoding";
    printf("Original String: %s\n", input_string);
    // 1. 编码
    encodeString(input_string, NULL); // root 参数在 encodeString 内部重新构建
    // 2. 解码
    decodeFile("encoded.bin");
    return 0;
}

如何编译和运行

  1. 将以上所有代码片段合并到一个 C 文件中,huffman.c
  2. 使用 GCC 编译:
    gcc huffman.c -o huffman
  3. 运行程序:
    ./huffman

预期输出

Original String: this is an example for huffman encoding
Huffman Code Table:
Character | Code (Binary)
-------------------------
    a     | 110
    c     | 01110
    d     | 01111
    e     | 100
    f     | 0010
    g     | 01011
    h     | 1110
    i     | 101
    l     | 01010
    m     | 00110
    n     | 1111
    o     | 00111
    p     | 00011
    r     | 00010
    s     | 0000
    t     | 0100
    u     | 00101
    x     | 00001
    space  | 0110
Encoding complete. Data saved to 'encoded.bin'.
Decoded String:
this is an example for huffman encoding

总结与注意事项

  • 内存管理:C 语言中手动内存管理至关重要,本示例中省略了一些 free() 调用(freeHuffmanTree),在实际应用中必须确保所有 malloc 都有对应的 free,否则会造成内存泄漏。
  • 文件格式:本示例将编码表和数据直接写入二进制文件,这是一种简单的方式,更复杂的压缩格式(如 ZIP)会有更高效的存储和元数据管理方式。
  • 效率:使用链表实现优先队列的时间复杂度是 O(n^2),对于大规模数据,应使用堆(Heap)结构来实现优先队列,可以将构建树的时间复杂度优化到 O(n log n)。
  • 字符集:本示例假设字符是 ASCII 字符,对于更广泛的字符集(如 Unicode),频率统计和节点存储的逻辑需要相应调整。

这个完整的 C 语言实现涵盖了 Huffman 编码的所有核心步骤,希望能帮助你理解其原理和实现方式。

-- 展开阅读全文 --
头像
dede 81 html编辑器
« 上一篇 01-04
织梦怎么调用一下个栏目
下一篇 » 01-04

相关文章

取消
微信二维码
支付宝二维码

目录[+]