17. 文件压缩与解压实战:基于游程编码的简单压缩算法
说到文件压缩,大家可能首先想到的是 ZIP、RAR 这些。但它们的原理其实挺复杂的,涉及哈夫曼编码、LZ77 这些高级算法。今天咱们聊一个更简单的——游程编码(Run-Length Encoding,RLE)。这玩意儿虽然简单,但在某些场景下特别好用。我早年做嵌入式开发时,就曾用 RLE 压缩过传感器采集的波形数据,效果出奇的好。
17.1 什么是游程编码?
说白了,游程编码的核心思想就一句话:把连续重复的数据,用「重复次数 + 数据值」来表示。
举个例子,假设你有一串数据:
AAAAABBBCCCCDDDDD
用 RLE 压缩后,就变成了:
5A3B4C5D
你看,原来 16 个字符,现在变成了 8 个字符。压缩率 50%。
为什么会这样?因为原始数据里有很多连续的重复字符。RLE 就是抓住了这个规律。
适用场景:图像(尤其是二值图像)、简单图形、传感器数据、文本中的连续空格等。
不适用场景:随机数据、已经压缩过的文件(比如 JPEG、ZIP 文件)。
17.2 RLE 的两种常见编码格式
我在项目中见过两种主流的 RLE 实现方式,各有优劣。
| 编码格式 | 表示方式 | 优点 | 缺点 |
|---|---|---|---|
| 格式一:计数+数据 | 5A 3B 4C | 简单直观,压缩率高 | 计数可能占用额外字节 |
| 格式二:标记+计数+数据 | FF 05 41 | 可处理非重复数据 | 需要特殊标记,略复杂 |
我个人习惯用格式一,因为它实现起来最直接。但要注意,如果数据本身没有重复,RLE 反而会让文件变大。你想想看,原本一个字节的数据,现在变成了「计数+数据」两个字节,那不是亏了吗?
避坑指南:我曾经在一个项目里,没检查数据特征就直接用了 RLE。结果压缩后的文件比原文件还大 30%。后来我加了一个判断:如果压缩后的数据比原数据大,就放弃压缩,直接存储原始数据。
17.3 用 C 语言实现 RLE 压缩
好,咱们直接上代码。下面是一个简单的 RLE 压缩函数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// RLE 压缩函数
// 输入:src - 原始数据,src_len - 原始数据长度
// 输出:dst - 压缩后数据,dst_len - 压缩后数据长度
// 返回值:0 成功,-1 失败
int rle_compress(const unsigned char *src, int src_len,
unsigned char *dst, int *dst_len) {
if (!src || !dst || !dst_len || src_len <= 0) {
return -1;
}
int src_pos = 0; // 原始数据当前位置
int dst_pos = 0; // 压缩数据当前位置
while (src_pos < src_len) {
unsigned char current = src[src_pos];
int count = 1;
// 统计连续重复的字节数
while (src_pos + count < src_len &&
src[src_pos + count] == current &&
count < 255) { // 计数不能超过 255
count++;
}
// 写入计数(1 字节)
dst[dst_pos++] = (unsigned char)count;
// 写入数据(1 字节)
dst[dst_pos++] = current;
src_pos += count;
}
*dst_len = dst_pos;
return 0;
}
嗯,这里要注意几个细节:
- 计数限制:我用的是 1 字节计数,所以最大重复次数是 255。如果连续重复超过 255 个,需要拆分成多段。
- 边界检查:写入 dst 时,要确保不会越界。实际项目中,dst 的大小应该至少是 src_len * 2。
- 性能优化:这个实现是 O(n) 的,一次遍历就能完成。对于大文件,性能还不错。
17.4 解压:把数据还原回去
压缩容易,解压更简单。就是读一个计数,再读一个数据,然后重复写入计数次。
// RLE 解压函数
// 输入:src - 压缩数据,src_len - 压缩数据长度
// 输出:dst - 解压后数据,dst_len - 解压后数据长度
// 返回值:0 成功,-1 失败
int rle_decompress(const unsigned char *src, int src_len,
unsigned char *dst, int *dst_len) {
if (!src || !dst || !dst_len || src_len <= 0) {
return -1;
}
int src_pos = 0;
int dst_pos = 0;
while (src_pos < src_len) {
// 读取计数
int count = src[src_pos++];
// 读取数据
unsigned char data = src[src_pos++];
// 重复写入 count 次
for (int i = 0; i < count; i++) {
dst[dst_pos++] = data;
}
}
*dst_len = dst_pos;
return 0;
}
你看,解压代码比压缩还短。这就是 RLE 的魅力——简单、高效。
小技巧:如果你要压缩的是文本文件,可以先把文本中的连续空格、连续换行符找出来。这些往往是 RLE 的「肥肉」。我在处理日志文件时,经常用这招,压缩率能到 60% 以上。
17.5 完整示例:压缩一个文件
下面是一个完整的示例,演示如何用 RLE 压缩一个文件,并保存到新文件中。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *fp_in = fopen("input.txt", "rb");
if (!fp_in) {
perror("打开输入文件失败");
return 1;
}
// 获取文件大小
fseek(fp_in, 0, SEEK_END);
long file_size = ftell(fp_in);
fseek(fp_in, 0, SEEK_SET);
// 读取原始数据
unsigned char *src = (unsigned char *)malloc(file_size);
fread(src, 1, file_size, fp_in);
fclose(fp_in);
// 分配压缩缓冲区(最坏情况:每个字节都变成 2 字节)
unsigned char *dst = (unsigned char *)malloc(file_size * 2);
int dst_len = 0;
// 执行压缩
if (rle_compress(src, file_size, dst, &dst_len) != 0) {
printf("压缩失败\n");
free(src);
free(dst);
return 1;
}
// 写入压缩文件
FILE *fp_out = fopen("output.rle", "wb");
fwrite(dst, 1, dst_len, fp_out);
fclose(fp_out);
printf("原始大小: %ld 字节\n", file_size);
printf("压缩后大小: %d 字节\n", dst_len);
printf("压缩率: %.2f%%\n", (1.0 - (double)dst_len / file_size) * 100);
free(src);
free(dst);
return 0;
}
17.6 RLE 的局限性
说实话,RLE 并不是万能的。我遇到过一些情况,RLE 完全派不上用场:
- 随机数据:比如加密后的数据,几乎没有连续重复的字节。用 RLE 压缩,文件反而会膨胀一倍。
- 已压缩数据:JPEG、PNG、ZIP 这些文件内部已经做过压缩了,再用 RLE 基本没效果。
- 二进制可执行文件:.exe、.dll 这些文件,数据分布比较均匀,RLE 效果很差。
所以,在实际项目中,我通常会先对数据做个「预分析」。如果连续重复的字节比例低于某个阈值(比如 10%),我就直接放弃 RLE,改用其他算法。
17.7 知识体系总览
下面这张图,帮你理清 RLE 的核心逻辑和实现要点。
17.8 总结
RLE 虽然简单,但它是理解更复杂压缩算法的基础。你想想看,哈夫曼编码、LZ77 这些算法,本质上也是在寻找数据中的「规律」和「重复」。RLE 只是把这种思想做到了极致简单。
我个人建议,如果你刚开始接触文件压缩,不妨先从 RLE 入手。写一个完整的压缩/解压工具,跑通流程,再去看那些更复杂的算法。这样循序渐进,理解会深刻得多。
好了,这一章的内容就到这里。记住:简单不等于没用。RLE 在特定场景下,依然是一个高效、可靠的压缩方案。