哈夫曼编码 C++ 实现:优先队列构建 6 字符编码树(附完整代码)
在数据压缩领域,哈夫曼编码是一种经典的无损压缩算法。它通过统计字符出现频率,构建最优二叉树来实现高效编码。本文将深入探讨如何使用 C++ 标准模板库(STL)中的优先队列(priority_queue)来实现哈夫曼编码,并提供一个完整的、可直接编译运行的代码示例。
1. 哈夫曼编码基础原理
哈夫曼编码的核心思想是变长前缀编码,即出现频率高的字符使用较短的编码,频率低的字符使用较长的编码。这种编码方式需要满足两个关键特性:
- 前缀属性:任何字符的编码都不是另一个字符编码的前缀
- 最优压缩:整体编码长度最短,压缩效率最高
构建哈夫曼树的基本步骤如下:
- 统计每个字符的出现频率
- 将每个字符视为一个节点,频率作为权值
- 每次从节点集合中选取权值最小的两个节点合并
- 将合并后的新节点(权值为两节点之和)加入集合
- 重复上述过程直到只剩一个节点,形成完整的哈夫曼树
编码生成规则:从根节点出发,向左子树走记为0,向右子树走记为1,到达叶节点时的路径即为该字符的编码。
2. C++ 实现的关键数据结构
2.1 哈夫曼树节点设计
我们首先需要定义哈夫曼树的节点结构。每个节点需要存储以下信息:
struct HuffmanNode { char data; // 字符数据(叶节点有效) unsigned freq; // 字符频率/权值 HuffmanNode *left; // 左子节点指针 HuffmanNode *right; // 右子节点指针 // 构造函数 HuffmanNode(char data, unsigned freq) : data(data), freq(freq), left(nullptr), right(nullptr) {} };2.2 优先队列的比较函数
STL的priority_queue默认实现最大堆,而我们需要最小堆。可以通过自定义比较函数实现:
struct Compare { bool operator()(HuffmanNode* l, HuffmanNode* r) { return l->freq > r->freq; // 小顶堆 } }; // 优先队列类型定义 using MinHeap = priority_queue<HuffmanNode*, vector<HuffmanNode*>, Compare>;3. 完整实现步骤
3.1 构建哈夫曼树
HuffmanNode* buildHuffmanTree(const unordered_map<char, unsigned>& freqMap) { MinHeap minHeap; // 1. 创建叶节点并加入优先队列 for (auto& pair : freqMap) { minHeap.push(new HuffmanNode(pair.first, pair.second)); } // 2. 循环合并节点直到只剩一个 while (minHeap.size() != 1) { // 取出两个最小频率节点 HuffmanNode* left = minHeap.top(); minHeap.pop(); HuffmanNode* right = minHeap.top(); minHeap.pop(); // 创建新内部节点('$'标记内部节点) HuffmanNode* top = new HuffmanNode('$', left->freq + right->freq); top->left = left; top->right = right; minHeap.push(top); } return minHeap.top(); // 返回根节点 }3.2 生成编码表
通过深度优先遍历(DFS)生成每个字符的编码:
void generateCodes(HuffmanNode* root, const string& str, unordered_map<char, string>& huffmanCode) { if (!root) return; // 叶节点存储编码 if (!root->left && !root->right) { huffmanCode[root->data] = str; } generateCodes(root->left, str + "0", huffmanCode); generateCodes(root->right, str + "1", huffmanCode); }3.3 编码字符串
利用生成的编码表对输入字符串进行编码:
string encodeString(const string& input, const unordered_map<char, string>& huffmanCode) { string encoded; for (char ch : input) { encoded += huffmanCode.at(ch); } return encoded; }3.4 解码字符串
根据哈夫曼树解码二进制串:
string decodeString(HuffmanNode* root, const string& encoded) { string decoded; HuffmanNode* curr = root; for (char bit : encoded) { curr = (bit == '0') ? curr->left : curr->right; // 到达叶节点 if (!curr->left && !curr->right) { decoded += curr->data; curr = root; } } return decoded; }4. 完整可运行示例
下面是一个处理6个字符(a-f)的完整示例程序:
#include <iostream> #include <queue> #include <unordered_map> #include <string> using namespace std; // 哈夫曼节点结构 struct HuffmanNode { char data; unsigned freq; HuffmanNode *left, *right; HuffmanNode(char data, unsigned freq) : data(data), freq(freq), left(nullptr), right(nullptr) {} }; // 优先队列比较函数 struct Compare { bool operator()(HuffmanNode* l, HuffmanNode* r) { return l->freq > r->freq; } }; using MinHeap = priority_queue<HuffmanNode*, vector<HuffmanNode*>, Compare>; // 构建哈夫曼树 HuffmanNode* buildHuffmanTree(const unordered_map<char, unsigned>& freqMap) { MinHeap minHeap; for (auto& pair : freqMap) { minHeap.push(new HuffmanNode(pair.first, pair.second)); } while (minHeap.size() != 1) { HuffmanNode* left = minHeap.top(); minHeap.pop(); HuffmanNode* right = minHeap.top(); minHeap.pop(); HuffmanNode* top = new HuffmanNode('$', left->freq + right->freq); top->left = left; top->right = right; minHeap.push(top); } return minHeap.top(); } // 生成编码表 void generateCodes(HuffmanNode* root, const string& str, unordered_map<char, string>& huffmanCode) { if (!root) return; if (!root->left && !root->right) { huffmanCode[root->data] = str; } generateCodes(root->left, str + "0", huffmanCode); generateCodes(root->right, str + "1", huffmanCode); } // 编码字符串 string encodeString(const string& input, const unordered_map<char, string>& huffmanCode) { string encoded; for (char ch : input) { encoded += huffmanCode.at(ch); } return encoded; } // 解码字符串 string decodeString(HuffmanNode* root, const string& encoded) { string decoded; HuffmanNode* curr = root; for (char bit : encoded) { curr = (bit == '0') ? curr->left : curr->right; if (!curr->left && !curr->right) { decoded += curr->data; curr = root; } } return decoded; } int main() { // 示例:6个字符的频率 unordered_map<char, unsigned> freqMap = { {'a', 6}, {'b', 3}, {'c', 8}, {'d', 2}, {'e', 10}, {'f', 4} }; // 1. 构建哈夫曼树 HuffmanNode* root = buildHuffmanTree(freqMap); // 2. 生成编码表 unordered_map<char, string> huffmanCode; generateCodes(root, "", huffmanCode); cout << "Huffman Codes:\n"; for (auto& pair : huffmanCode) { cout << pair.first << ": " << pair.second << endl; } // 3. 编码示例字符串 string testStr = "abcdef"; string encoded = encodeString(testStr, huffmanCode); cout << "\nOriginal string: " << testStr << endl; cout << "Encoded string: " << encoded << endl; // 4. 解码验证 string decoded = decodeString(root, encoded); cout << "Decoded string: " << decoded << endl; // 计算压缩率 int originalBits = testStr.length() * 8; // 假设原始ASCII编码 int compressedBits = encoded.length(); double ratio = 100.0 * (originalBits - compressedBits) / originalBits; cout << "\nCompression ratio: " << ratio << "%" << endl; return 0; }5. 性能分析与优化
5.1 时间复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 构建优先队列 | O(n log n) | n为不同字符数量 |
| 构建哈夫曼树 | O(n log n) | 每次堆操作O(log n),共n-1次 |
| 生成编码表 | O(n) | 树遍历 |
| 编码字符串 | O(m) | m为输入字符串长度 |
| 解码字符串 | O(k) | k为编码后二进制串长度 |
5.2 空间优化技巧
- 内存管理:示例代码中未释放节点内存,实际应用中应添加析构函数递归删除树节点
- 位操作优化:编码结果可使用bitset而非字符串存储,节省空间
- 频率表压缩:对于大型文件,可使用更紧凑的频率表表示方法
5.3 扩展功能
// 添加内存释放函数 void deleteTree(HuffmanNode* root) { if (!root) return; deleteTree(root->left); deleteTree(root->right); delete root; } // 在main函数结束前调用 deleteTree(root);6. 实际应用与测试
6.1 测试用例设计
考虑以下测试场景:
常规情况:
unordered_map<char, unsigned> freqMap1 = { {'a', 5}, {'b', 9}, {'c', 12}, {'d', 13}, {'e', 16}, {'f', 45} };极端情况:
// 所有字符频率相同 unordered_map<char, unsigned> freqMap2 = { {'a', 1}, {'b', 1}, {'c', 1}, {'d', 1}, {'e', 1}, {'f', 1} }; // 一个字符占绝对多数 unordered_map<char, unsigned> freqMap3 = { {'a', 99}, {'b', 1}, {'c', 1}, {'d', 1}, {'e', 1}, {'f', 1} };
6.2 编码结果验证
对于频率表freqMap1,程序输出示例:
Huffman Codes: f: 0 c: 100 d: 101 a: 1100 b: 1101 e: 111 Original string: abcdef Encoded string: 11001101111001010 Decoded string: abcdef Compression ratio: 42.1875%6.3 边界条件处理
在实际应用中需要考虑:
- 空输入处理
- 单字符重复情况
- 非ASCII字符支持
- 大文件分块处理
// 空输入检查示例 if (freqMap.empty()) { cerr << "Error: Empty frequency map!" << endl; return nullptr; }7. 进阶实现技巧
7.1 使用更高效的数据结构
对于大规模数据,可以考虑以下优化:
- 使用数组存储哈夫曼树:对于已知字符集大小的情况,可以用数组而非指针表示树结构
- 位级操作:使用位运算而非字符串拼接来提高编码效率
7.2 并行化处理
对于超大文件,可以:
- 分块统计字符频率
- 并行构建哈夫曼树
- 多线程进行编码操作
7.3 文件压缩实战
将上述算法扩展为实际文件压缩工具需要:
- 添加文件IO处理
- 序列化哈夫曼树结构
- 处理二进制数据而非文本
- 添加压缩文件头信息
// 简化的文件头结构示例 struct FileHeader { uint32_t magicNumber = 0x484D414E; // "HMAN" uint32_t originalSize; uint32_t tableSize; // 后面跟着频率表和编码数据 };通过本实现的完整哈夫曼编码方案,开发者可以深入理解数据压缩的核心原理,并将其应用于实际项目中。代码中使用的优先队列方案既保持了清晰性,又具有良好的性能表现,是学习算法与数据结构结合的优秀范例。