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 的核心逻辑和实现要点。

游程编码(RLE)知识体系 核心概念 将连续重复数据表示为「计数 + 数据值」 格式一:计数+数据 例:5A 3B 4C 格式二:标记+计数+数据 例:FF 05 41 压缩:遍历 + 计数 + 写入 解压:读取 + 循环写入 ⚠ 注意:计数上限255、边界检查、预分析数据特征

17.8 总结

RLE 虽然简单,但它是理解更复杂压缩算法的基础。你想想看,哈夫曼编码、LZ77 这些算法,本质上也是在寻找数据中的「规律」和「重复」。RLE 只是把这种思想做到了极致简单。

我个人建议,如果你刚开始接触文件压缩,不妨先从 RLE 入手。写一个完整的压缩/解压工具,跑通流程,再去看那些更复杂的算法。这样循序渐进,理解会深刻得多。

好了,这一章的内容就到这里。记住:简单不等于没用。RLE 在特定场景下,依然是一个高效、可靠的压缩方案。


公众号:蓝海资料掘金营,微信 deep3321