生长中技术笔记
用 C 语言实现 LZ77 编码与解码的示例代码
背景与适用场景
LZ77 是 1977 年由 Lempel 和 Ziv 提出的无损压缩算法,核心思想是:在一段已读入的数据(滑动窗口)中查找与当前位置重复的子串,用"偏移量 + 长度 + 下一个字符"三元组替代重复内容。GZIP、PNG、ZIP 等格式底层都使用了 LZ77 或其变体(LZ77 + Huffman)。
本文给出一个最简的 C 语言实现,目的是帮助读者理解编码与解码的完整流程,而非用于生产环境。适合嵌入式开发、系统编程或算法学习阶段的读者参考。
基本原理
编码(压缩)
- 维护一个固定大小的滑动窗口,存放已处理的输入字节。
- 对当前待编码位置,在窗口中搜索与后续输入匹配的最长子串。
- 若找到匹配(长度 > 0),输出 (offset, length, next_char);否则输出 (0, 0, current_char)。
- 将已编码的字节追加到窗口,窗口满时整体左移。
解码(解压)
- 逐条读取编码单元。
- 若 length == 0,直接输出 next_char。
- 若 length > 0,根据 offset 从窗口中取出 length 个字节,再输出 next_char。
- 每输出一个字节就追加到窗口。
编码流程要点
- 窗口大小决定可回看的距离,缓冲区大小决定一次 I/O 的粒度。
- 匹配搜索是 O(window_size × buffer_size) 量级的暴力扫描(内层 while 还会随匹配长度线性增长),实际实现通常使用哈希表或后缀树加速。
- 编码单元在本文用
struct { int offset; int length; char next; }表示,直接以二进制写入文件。
解码流程要点
- 解码时必须保证 offset 指向的窗口位置已经写入(即解码顺序与编码顺序一致)。
- 当匹配子串与输出位置重叠时(例如 "ababab" 中 "ab" 的重复),需要逐字节复制而非
memcpy,否则可能读到尚未写入的数据。
代码实现
text
代码中的已知限制与风险
以下问题在示例代码中存在,阅读和二次开发时需要注意:
边界与越界
while循环的边界检查放在循环体内(先比较、再判断是否越界),逻辑上可以防止 while 条件越界;但code.next = buffer[i + max_length]在max_length恰好等于buffer_size - i时会访问buffer[buffer_size],缺少显式保护。- 解码时
window[window_size - code.offset + k]未校验window_size - code.offset >= 0,若编码端写入了非法 offset,解码端会越界读取。
窗口更新效率
- 窗口满时每次追加都调用
memmove搬移整个窗口(O(WINDOW_SIZE)),在高频写入场景下开销显著。生产实现通常使用环形缓冲区(ring buffer)避免整体搬移。
平台与可移植性
LZ77Code结构体直接fwrite/fread,其内存布局受编译器对齐、字节序(大端 / 小端)和int宽度影响,跨平台或跨编译器时不保证兼容。若需持久化或网络传输,应使用显式序列化(如htonl/ntohl或固定宽度类型uint32_t)。
其他
- 代码未包含
main函数,也未处理fread/fwrite的返回值错误。 - 编码循环中
i += max_length之后for循环还会执行i++,实际跳过了max_length + 1个字符,与"匹配子串 + 下一个字符"的语义一致,但阅读时容易误判。 - 未处理输入文件为空或编码单元在文件末尾被截断的情况。
验证建议
- 准备一段包含明显重复模式的短文本(如 "abcabcabcabc"),手动推演编码单元序列,再与程序输出对比。
- 对同一输入执行 encode → decode,用
cmp或diff比较原始文件与解码结果是否一致。 - 在 AddressSanitizer(
-fsanitize=address)或 Valgrind 下运行,检查是否存在越界读写。 - 测试边界输入:空文件、单字节文件、恰好填满窗口的文件、窗口满后继续写入的文件。
小结
本文的示例覆盖了 LZ77 编码与解码的核心逻辑:滑动窗口、暴力匹配、三元组输出、逐字节回写。它适合作为理解算法流程的起点,但在边界保护、I/O 错误处理、窗口更新效率和跨平台序列化方面仍需补强,才能用于实际项目。
<!-- csdn-article-id: 131181351 -->本文最初于 2023/6/13 发布在 CSDN。
评论
正在读取评论…