哈夫曼编码实现的关键在于正确处理比特流的读写:需补零对齐、记录填充位数、用位掩码逐比特操作,避免使用std::bitset;建树要用std::priority_queue配std::greater,仅叶子节点生成编码,结果存map供O(1)查找。

哈夫曼树的核心是优先队列,不是手写排序
用 std::priority_queue 实现最小堆,比手动维护数组或链表高效得多。C++ 默认是最大堆,必须显式传入 std::greater 或自定义比较器,否则节点会按权重从大到小弹出,建树直接失败。
常见错误:只重载 operator 但没注意优先级方向——哈夫曼要求每次取两个**最小**权重的节点合并,所以比较逻辑必须让小权重“优先”被 pop。
- 节点结构体里不要只存
weight,必须带left、right指针(或shared_ptr),否则无法回溯生成编码 - 优先队列类型要统一:所有元素都得是
shared_ptr,避免裸指针生命周期失控 - 字符频次统计建议用
std::map,能覆盖全部 256 个字节值,不漏空格、换行等控制字符
构建过程必须合并节点,不能复用原始叶子节点
每轮从队列取两个节点,新建一个父节点,其 weight 为二者之和,左右子指针分别指向取出的节点。这个新节点再 push 回队列。重复直到队列只剩一个节点——它就是哈夫曼树根。
关键点:每次合并后,原来的两个节点不再是独立叶子,而是新节点的子树。如果误将原始字符节点反复加入队列(比如忘了 pop 掉已合并的),会导致无限循环或树结构错乱。
立即学习“C++免费学习笔记(深入)”;
struct Node {
unsigned char ch;
int weight;
std::shared_ptr left, right;
Node(unsigned char c, int w) : ch(c), weight(w) {}
Node(int w, std::shared_ptr l, std::shared_ptr r)
: weight(w), left(l), right(r) {}
};
auto cmp = [](const std::shared_ptr& a, const std::shared_ptr& b) {
return a->weight > b->weight; // 小权重要先出,所以用 >
};
std::priority_queue, std::vector>, decltype(cmp)> pq(cmp);
生成编码要用 DFS 递归,别用 BFS
哈夫曼编码本质是根到叶子的路径,左分支记 0、右分支记 1。DFS 天然适合边遍历边拼接编码字符串;BFS 虽然也能做,但需额外保存路径状态,容易出错且无优势。
注意终止条件:只在 node->left == nullptr && node->right == nullptr 时才记录编码(即叶子节点)。中间节点没有对应字符,不能输出。
- 递归参数建议传
const std::shared_ptr+ 当前编码字符串& std::string code,避免拷贝开销 - 编码结果存进
std::map,后续压缩时可 O(1) 查找 - 如果输入数据含未出现的字符(如全英文文本里有中文),该字符不会出现在 map 中,查表前务必检查
find()是否有效
压缩/解压时边界问题最常引发崩溃
哈夫曼编码长度不固定,最终比特流往往不是字节对齐的。写文件时必须补零并记录填充位数;读文件时先读头部的填充数,再逐比特解析——否则最后一个字节可能被截断或误读。
典型现象:压缩后文件比原文件还大,或解压出乱码。根本原因常是比特读写逻辑没处理好末尾字节的掩码与偏移。
- 写入时用
std::vector缓存字节,用bit_count记录当前字节已写几位,超 8 位就进位 - 读取时同样维护
bit_pos(0–7)和当前字节,用(byte >> (7 - bit_pos)) & 1提取单比特 - 不要试图用
std::bitset做流式处理——它不支持动态追加和非整字节读取,反而增加复杂度
真正难的不是建树,是把变长比特串可靠地落盘和还原。这部分逻辑一旦写错,调试成本远高于树构建本身。











