哈夫曼编码实现的关键在于正确处理比特流的读写:需补零对齐、记录填充位数、用位掩码逐比特操作,避免使用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 掉已合并的),会导致无限循环或树结构错乱。
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);
哈夫曼编码本质是根到叶子的路径,左分支记 0、右分支记 1。DFS 天然适合边遍历边拼接编码字符串;BFS 虽然也能做,但需额外保存路径状态,容易出错且无优势。
注意终止条件:只在 node->left == nullptr && node->right == nullptr 时才记录编码(即叶子节点)。中间节点没有对应字符,不能输出。
const std::shared_ptr& + 当前编码字符串 std::string code,避免拷贝开销std::map,后续压缩时可 O(1) 查找find() 是否有效哈夫曼编码长度不固定,最终比特流往往不是字节对齐的。写文件时必须补零并记录填充位数;读文件时先读头部的填充数,再逐比特解析——否则最后一个字节可能被截断或误读。
典型现象:压缩后文件比原文件还大,或解压出乱码。根本原因常是比特读写逻辑没处理好末尾字节的掩码与偏移。
std::vector 缓存字节,用 bit_count 记录当前字节已写几位,超 8 位就进位bit_pos(0–7)和当前字节,用 (byte >> (7 - bit_pos)) & 1 提取单比特std::bitset 做流式处理——它不支持动态追加和非整字节读取,反而增加复杂度真正难的不是建树,是把变长比特串可靠地落盘和还原。这部分逻辑一旦写错,调试成本远高于树构建本身。