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

(图片来源网络,侵删)
实现 Huffman 编码通常需要以下几个步骤:
- 统计频率:统计原始数据中每个字符出现的频率。
- 构建 Huffman 树:根据频率构建一棵最优二叉树。
- 生成编码表:遍历 Huffman 树,为每个字符生成其对应的二进制编码。
- 编码:根据编码表,将原始数据转换为二进制编码流。
- 解码:根据 Huffman 树,将二进制编码流还原为原始数据。
下面我们分步用 C 语言实现这个过程。
数据结构定义
我们需要定义几种核心数据结构。
a) Huffman 树节点
Huffman 树的每个节点都是一个结构体,它包含:

(图片来源网络,侵删)
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),其中前半部分是编码表,后半部分是数据。

(图片来源网络,侵删)
// 打印编码表(用于调试)
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;
}
如何编译和运行
- 将以上所有代码片段合并到一个 C 文件中,
huffman.c。 - 使用 GCC 编译:
gcc huffman.c -o huffman
- 运行程序:
./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 编码的所有核心步骤,希望能帮助你理解其原理和实现方式。
