每日算法 — 使用java实现游程编码:Run-Length Encoding与位打包压缩

游程编码(Run-Length Encoding,RLE)是最简单且历史最悠久的无损数据压缩算法之一。它的核心思想极其直观:将连续重复的字符序列替换为”字符+重复次数”的二元组。虽然RLE本身对一般文本的压缩效果有限,但在处理具有大量连续重复数据的场景(如简单图像、BWT变换后的数据、传真图像)时表现出色。

算法核心思想

RLE基于一个简单观察:许多类型的数据中存在大量连续相同的符号。例如:
– 二值图像中大片连续的黑白像素
– BWT变换后的数据中聚集的相同字符
– 简单图形的扫描线数据

编码过程

扫描输入数据,将连续重复的字符替换为(字符, 计数)的形式。

示例:
– 原始:AAAABBBCCDAA
– 编码:A4B3C2D1A2

解码过程

遍历编码序列,将每个(字符, 计数)展开为对应数量的字符。

位打包优化

对于二值数据(只有0和1),可以进一步优化:
– 使用变长编码表示连续的0和1的个数
– 或者固定使用N位存储计数,最大化压缩效率

Java完整实现

import java.util.*;

/**
 * 游程编码(RLE)实现
 * 包含基础RLE、位打包优化和二值图像专用版本
 */
public class RunLengthEncoding {

    /**
     * 基础RLE编码
     * @param input 原始字节数组
     * @return 编码后的(RLE符号)列表
     */
    public List<Run> encode(byte[] input) {
        List<Run> result = new ArrayList<>();
        if (input == null || input.length == 0) {
            return result;
        }

        byte current = input[0];
        int count = 1;

        for (int i = 1; i < input.length; i++) {
            if (input[i] == current && count < 255) {
                // 继续当前游程,上限255(用一个字节存储)
                count++;
            } else {
                // 结束当前游程,开始新游程
                result.add(new Run(current, count));
                current = input[i];
                count = 1;
            }
        }

        // 添加最后一个游程
        result.add(new Run(current, count));
        return result;
    }

    /**
     * 基础RLE解码
     */
    public byte[] decode(List<Run> encoded) {
        // 先计算总长度
        int totalLength = 0;
        for (Run run : encoded) {
            totalLength += run.count;
        }

        byte[] result = new byte[totalLength];
        int pos = 0;

        for (Run run : encoded) {
            Arrays.fill(result, pos, pos + run.count, run.value);
            pos += run.count;
        }

        return result;
    }

    /**
     * 将RLE结果打包为紧凑的字节数组
     * 格式:[值1, 计数1, 值2, 计数2, ...]
     */
    public byte[] packRuns(List<Run> runs) {
        byte[] packed = new byte[runs.size() * 2];
        for (int i = 0; i < runs.size(); i++) {
            packed[i * 2] = runs.get(i).value;
            packed[i * 2 + 1] = (byte) runs.get(i).count;
        }
        return packed;
    }

    /**
     * 从字节数组解包RLE结果
     */
    public List<Run> unpackRuns(byte[] packed) {
        List<Run> runs = new ArrayList<>();
        for (int i = 0; i < packed.length; i += 2) {
            runs.add(new Run(packed[i], packed[i + 1] & 0xFF));
        }
        return runs;
    }

    /**
     * 二值图像专用RLE(像素只有0和1)
     * 假设数据以0开始交替记录游程长度
     */
    public List<Integer> encodeBinary(byte[] input) {
        List<Integer> result = new ArrayList<>();
        if (input == null || input.length == 0) {
            return result;
        }

        byte current = input[0];
        int count = 1;

        for (int i = 1; i < input.length; i++) {
            if (input[i] == current) {
                count++;
            } else {
                result.add(count);
                current = input[i];
                count = 1;
            }
        }
        result.add(count);

        // 如果原始数据以1开始,在头部插入0
        if (input[0] == 1) {
            result.add(0, 0);
        }

        return result;
    }

    /**
     * 二值图像RLE解码
     * @param encoded 游程长度列表(假设以0开始交替)
     * @return 解码后的二值数据
     */
    public byte[] decodeBinary(List<Integer> encoded) {
        int totalLength = encoded.stream().mapToInt(Integer::intValue).sum();
        byte[] result = new byte[totalLength];
        int pos = 0;
        byte value = 0;

        for (int count : encoded) {
            Arrays.fill(result, pos, pos + count, value);
            pos += count;
            value = (byte) (1 - value); // 0和1交替
        }

        return result;
    }

    /**
     * 位打包编码:用位存储二值数据的游程
     * 使用Golomb-Rice编码存储游程长度
     */
    public BitPackedResult encodeBitPacked(byte[] input, int k) {
        List<Integer> runs = encodeBinary(input);

        // 使用k位存储商和余数(Golomb-Rice)
        // 对于参数m=2^k,编码q = n/m, r = n%m
        // q用一元编码,r用k位二进制

        StringBuilder bits = new StringBuilder();
        for (int run : runs) {
            int q = run >> k;       // 商
            int r = run & ((1 << k) - 1); // 余数

            // 一元编码商
            for (int i = 0; i < q; i++) {
                bits.append('1');
            }
            bits.append('0'); // 终止符

            // k位二进制编码余数
            String binary = Integer.toBinaryString(r);
            while (binary.length() < k) {
                binary = "0" + binary;
            }
            bits.append(binary);
        }

        return new BitPackedResult(bits.toString(), input[0] == 1, k);
    }

    /**
     * 计算压缩率
     */
    public double compressionRatio(byte[] original, List<Run> encoded) {
        int originalSize = original.length;
        int compressedSize = encoded.size() * 2; // 每个Run占2字节
        return 1.0 - (double) compressedSize / originalSize;
    }

    /**
     * RLE游程数据类
     */
    public static class Run {
        public final byte value;
        public final int count;

        public Run(byte value, int count) {
            this.value = value;
            this.count = count;
        }

        @Override
        public String toString() {
            return String.format("(%d,%d)", value & 0xFF, count);
        }
    }

    /**
     * 位打包结果封装
     */
    public static class BitPackedResult {
        public final String bitString;
        public final boolean startsWithOne;
        public final int k;

        public BitPackedResult(String bitString, boolean startsWithOne, int k) {
            this.bitString = bitString;
            this.startsWithOne = startsWithOne;
            this.k = k;
        }

        public int bitLength() {
            return bitString.length();
        }

        public int byteLength() {
            return (bitString.length() + 7) / 8;
        }
    }

    public static void main(String[] args) {
        RunLengthEncoding rle = new RunLengthEncoding();

        // 测试1:基础RLE(文本数据)
        String text = "AAAABBBCCDAAEEEEEEFFFFFFFF";
        byte[] textBytes = text.getBytes();
        System.out.println("=== 基础RLE测试 ===");
        System.out.println("原始数据: " + text);
        System.out.println("原始长度: " + textBytes.length + " 字节");

        List<Run> encoded = rle.encode(textBytes);
        System.out.println("RLE编码: " + encoded);

        byte[] packed = rle.packRuns(encoded);
        System.out.println("打包后长度: " + packed.length + " 字节");

        double ratio = rle.compressionRatio(textBytes, encoded);
        System.out.printf("压缩率: %.2f%%\n", ratio * 100);

        byte[] decoded = rle.decode(rle.unpackRuns(packed));
        System.out.println("解压验证: " + text.equals(new String(decoded)));

        // 测试2:二值图像RLE
        System.out.println("\n=== 二值图像RLE测试 ===");
        byte[] binaryImage = {
            0, 0, 0, 0, 0, 1, 1, 1,
            1, 1, 0, 0, 0, 1, 1, 1,
            1, 1, 1, 0, 0, 0, 0, 0
        };
        System.out.println("原始二值数据: " + Arrays.toString(binaryImage));

        List<Integer> binaryEncoded = rle.encodeBinary(binaryImage);
        System.out.println("二值RLE: " + binaryEncoded);

        byte[] binaryDecoded = rle.decodeBinary(binaryEncoded);
        System.out.println("解压验证: " + Arrays.equals(binaryImage, binaryDecoded));

        // 测试3:BWT后的数据(RLE的理想场景)
        System.out.println("\n=== BWT后数据RLE测试 ===");
        String bwtLike = "annb$aaacccdddddeeee";
        byte[] bwtBytes = bwtLike.getBytes();
        List<Run> bwtEncoded = rle.encode(bwtBytes);
        System.out.println("原始: " + bwtLike);
        System.out.println("RLE编码: " + bwtEncoded);
        double bwtRatio = rle.compressionRatio(bwtBytes, bwtEncoded);
        System.out.printf("压缩率: %.2f%%\n", bwtRatio * 100);
    }
}

算法变体与优化

游程预测编码

对于某些数据,可以存储当前游程与前一个游程的差值,进一步减小数值范围。

自适应RLE

根据数据的统计特性动态调整:
– 对高频字符使用更短的编码
– 对低频字符保留原始形式(避免负压缩)

与BWT的协同

RLE最常见的应用场景是BWT的后处理阶段。BWT将相似字符聚集后,RLE可以轻松将这些长游程压缩为极短编码。bzip2就是这一组合的经典代表。

复杂度分析

指标 复杂度 说明
编码时间 O(n) 单次线性扫描
解码时间 O(n) 线性展开
空间复杂度 O(n) 最坏情况下翻倍
最佳压缩率 接近100% 单一字符重复时
最差压缩率 -100% 无连续重复时,大小翻倍

实际应用

RLE广泛应用于:
– PCX图像格式:早期的PCX使用RLE压缩
– TIFF图像格式:支持多种RLE变体
– BMP图像格式:可选的RLE4和RLE8压缩
– 传真协议(G3/G4):使用改进的RLE处理黑白扫描线
– BZIP2后处理:BWT+MTF后的RLE阶段

总结

游程编码虽然简单,但深刻理解它对于学习压缩算法至关重要:
1. 简单高效:实现简洁,编解码速度极快
2. 场景依赖:压缩效果高度依赖数据特性
3. 组合威力:与BWT、MTF等算法组合可产生强大的压缩效果

RLE是理解数据冗余和压缩本质的最佳入门算法。