哈夫曼压缩需用 std::vector 或 std::string_view 处理原始字节流以避免 '\0' 截断;频次统计用 std::array,过滤零频字节;至少两个不同字节才建树;优先队列须小顶堆;编码生成用回溯避免 o(n²);解压需按位读取并缓存 buffer,头部存原始长度确保准确截断。

哈夫曼树构建时,字符频次统计必须覆盖全部输入字节
直接用 std::string 的 size() 和逐字符遍历统计是可行的,但容易漏掉空字符 '\0' —— 如果原始数据含二进制内容(比如压缩前是 std::vector<uint8_t></uint8_t>),用 std::string 会提前截断。实际应统一用 std::vector<uint8_t></uint8_t> 或 std::string_view(C++17+)配合 data() + size() 处理原始字节流。
频次数组建议用 std::array<size_t></size_t>,索引即字节值,避免 map 查找开销;若输入只含 ASCII 可优化为 128,但通用场景别省这点空间,否则遇到高位字节(如 UTF-8 多字节)会越界或统计错。
- 统计后需过滤频次为 0 的字节,否则建树时会生成无意义叶节点
- 若所有字节频次相同(比如全 0x41),哈夫曼树退化为链表,编码长度全为等长——此时压缩率 ≈ 0,应提前判断是否值得压缩
- 至少需要两个不同字节才能建出非退化树;单字符输入需特殊处理(直接存原字节+长度,不建树)
优先队列比较逻辑写错会导致树结构错误
用 std::priority_queue 构建哈夫曼树时,必须自定义比较器让频次小的节点优先出队。常见错误是写成 std::greater 却忘了节点本身要带频次字段,或比较器返回值反了——结果树根不是最小频次合并,编码位长全乱。
推荐写法:定义节点结构体含 freq 成员,然后用 lambda 或 functor 做 auto cmp = [](const Node* a, const Node* b) { return a->freq > b->freq; };,传入 std::priority_queue<node std::vector>, decltype(cmp)></node>。注意是 >,不是 ,因为 priority_queue 默认大顶堆,我们要小顶堆。
- 节点指针不能裸存,必须用智能指针或确保生命周期——临时节点在循环中 new 出来后,只被队列和父节点引用,出作用域前务必清空队列并 delete(或改用
std::unique_ptr) - 合并时两个子节点的左右顺序不影响解压正确性,但影响编码唯一性;通常左子树放“先出队”的节点,右子树放“后出队”的,这样生成的编码字典一致
编码表生成必须递归遍历且缓存路径,不能边走边拼接字符串
从根节点 DFS 生成每个字节的编码时,若每次递归都用 code + "0" 创建新字符串,深度为 N 时时间复杂度是 O(N²),对长文本极慢。正确做法是传入一个可变引用 std::string& current,进入左子树 push_back('0'),回溯时 pop_back(),右子树同理。
叶子节点命中时,把 current 拷贝进编码表 std::array<:string></:string> 对应位置即可。注意:非叶子节点不存编码,表中对应项保持为空字符串,后续编码时靠判空跳过。
- 编码表大小固定为 256,但仅频次 > 0 的字节有有效编码;解压时不依赖表大小,只靠比特流匹配
- 生成的编码必须满足前缀码性质——DFS 过程天然保证,无需额外校验;但若手动填表出错(比如重复赋值),会导致解压失败
- 编码长度最大可达 255 位(极端偏斜树),
std::string存没问题,但序列化到文件时得存长度字段,不能只存字符
解压过程必须按位读取且严格匹配前缀码,不能按字节切分
压缩后的数据本质是连续比特流,比如编码 "1011" 和 "0" 连起来是 10110。解压时如果按 8 位一组读 uint8_t 再转二进制字符串,再拼接查找,效率低且易错(末尾补零问题)。正确方式是维护一个缓冲区 uint64_t buffer 和当前有效位数 int bits_in_buffer,每次从文件/内存读一字节,左移填入 buffer 低位,然后 while 循环查编码表:从 buffer 高位取 1~max_len 位,查表命中即输出字节、移位、重置 bits_in_buffer。
关键点:buffer 至少 64 位,因为最长编码可能达 255 位,但实际只需支持当前树的最大编码长度(可预存 max_code_len)。查表用线性扫描即可(最多 256 项),比 trie 更快且省内存。
- 解压时若遇到 buffer 中剩余位数不足当前尝试长度,需继续读新字节填充——不要假设输入长度是 8 的倍数
- 最后一个字节可能只用了部分位(比如最后编码是 "10",但读入字节是 0b10110000),必须靠原始明文长度或结束标记截断,否则多解一个字节
- 最稳妥的结束机制是在压缩数据头部存原始字节数(
uint64_t),解压时计数到达即停,不依赖编码表能否匹配完全部 buffer
哈夫曼压缩的边界情况比想象中多:空输入、单字节、全同字节、含 \0 字节、跨字节边界读取、编码长度溢出……每一步的类型选择和边界检查都不能偷懒。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











