算术编码(Arithmetic Coding)是一种高效的无损熵编码技术,由Jorma Rissanen在1976年提出。与哈夫曼编码每次为整个符号分配整数位不同,算术编码将整个消息编码为一个位于[0,1)区间内的浮点数,可以实现 fractional bit allocation(小数位分配),在理论上无限接近信息熵极限。
算法核心思想
算术编码的核心思想是将整个输入序列映射到[0,1)区间内的一个子区间。每个符号根据其在当前概率模型中的概率占据对应比例的子区间。随着符号逐个处理,区间不断细分,最终输出该区间的二进制表示。
编码过程
- 初始化当前区间为
[low, high) = [0, 1) - 对于每个输入符号:
- 根据概率模型将该区间按比例划分为若干子区间
- 选择对应符号的子区间作为新的当前区间
- 处理完所有符号后,输出当前区间内的任意一个数(通常取区间中点)的二进制表示
解码过程
- 读取编码值(一个[0,1)区间内的数)
- 初始化区间为
[0, 1) - 循环:
- 根据当前区间和概率模型确定该编码值对应的符号
- 输出该符号
- 将该符号对应的子区间作为新的当前区间
- 重复直到解码出完整消息
Java完整实现
import java.util.*;
/**
* 算术编码实现
* 基于区间细分的熵编码,支持静态和自适应概率模型
*/
public class ArithmeticCoding {
// 使用整数运算避免浮点精度问题
// 用[0, WHOLE)表示[0, 1)区间
private static final long WHOLE = 1L << 32; // 2^32
private static final long HALF = WHOLE / 2;
private static final long QUARTER = WHOLE / 4;
/**
* 频率表:统计每个符号的出现次数
*/
public static class FrequencyTable {
private final long[] frequencies;
private long total;
public FrequencyTable(int symbolCount) {
this.frequencies = new long[symbolCount];
// 初始化每个符号至少出现1次(避免零概率)
for (int i = 0; i < symbolCount; i++) {
frequencies[i] = 1;
}
this.total = symbolCount;
}
public void increment(int symbol) {
frequencies[symbol]++;
total++;
}
public long getLow(int symbol) {
long sum = 0;
for (int i = 0; i < symbol; i++) {
sum += frequencies[i];
}
return sum;
}
public long getHigh(int symbol) {
return getLow(symbol) + frequencies[symbol];
}
public long getTotal() {
return total;
}
public int getSymbol(long value, long scale) {
long target = value * total / scale;
long sum = 0;
for (int i = 0; i < frequencies.length; i++) {
sum += frequencies[i];
if (target < sum) {
return i;
}
}
return frequencies.length - 1;
}
}
/**
* 算术编码结果
*/
public static class EncodedResult {
public final byte[] data;
public final long length;
public final int symbolCount;
public EncodedResult(byte[] data, long length, int symbolCount) {
this.data = data;
this.length = length;
this.symbolCount = symbolCount;
}
}
/**
* 静态算术编码
* @param input 输入字节数组
* @return 编码结果
*/
public EncodedResult encode(byte[] input) {
if (input.length == 0) {
return new EncodedResult(new byte[0], 0, 256);
}
// 构建静态频率表
FrequencyTable freq = new FrequencyTable(256);
for (byte b : input) {
freq.increment(b & 0xFF);
}
// 重新初始化用于编码(保持相同比例)
long low = 0;
long high = WHOLE;
long pendingBits = 0;
StringBuilder bitStream = new StringBuilder();
for (byte b : input) {
int symbol = b & 0xFF;
long range = high - low;
long symbolLow = freq.getLow(symbol);
long symbolHigh = freq.getHigh(symbol);
long total = freq.getTotal();
// 区间细分
high = low + range * symbolHigh / total;
low = low + range * symbolLow / total;
// 输出确定的位(E1/E2缩放)
while (true) {
if (high <= HALF) {
// E1: 区间完全在下半部分
bitStream.append('0');
appendPendingBits(bitStream, pendingBits, '1');
pendingBits = 0;
low = low * 2;
high = high * 2;
} else if (low >= HALF) {
// E2: 区间完全在上半部分
bitStream.append('1');
appendPendingBits(bitStream, pendingBits, '0');
pendingBits = 0;
low = (low - HALF) * 2;
high = (high - HALF) * 2;
} else if (low >= QUARTER && high <= 3 * QUARTER) {
// E3: 区间跨越中点
pendingBits++;
low = (low - QUARTER) * 2;
high = (high - QUARTER) * 2;
} else {
break;
}
}
}
// 输出最终区间信息
pendingBits++;
if (low <= QUARTER) {
bitStream.append('0');
appendPendingBits(bitStream, pendingBits, '1');
} else {
bitStream.append('1');
appendPendingBits(bitStream, pendingBits, '0');
}
// 将位流转为字节数组
byte[] data = bitsToBytes(bitStream.toString());
return new EncodedResult(data, input.length, 256);
}
/**
* 算术解码
*/
public byte[] decode(EncodedResult encoded, long originalLength) {
String bitStream = bytesToBits(encoded.data);
// 重建频率表(假设均匀初始化,实际应与编码端一致)
FrequencyTable freq = new FrequencyTable(encoded.symbolCount);
// 注:实际应用中频率表需要随解码同步更新或预先传输
byte[] result = new byte[(int) originalLength];
long low = 0;
long high = WHOLE;
long value = 0;
// 读取前32位初始化value
int bitPos = 0;
for (int i = 0; i < 32 && i < bitStream.length(); i++) {
value = (value << 1) | (bitStream.charAt(i) - '0');
bitPos++;
}
for (int i = 0; i < originalLength; i++) {
long range = high - low;
long total = freq.getTotal();
// 确定当前value对应的符号
int symbol = freq.getSymbol(value - low, range);
result[i] = (byte) symbol;
// 更新区间
long symbolLow = freq.getLow(symbol);
long symbolHigh = freq.getHigh(symbol);
high = low + range * symbolHigh / total;
low = low + range * symbolLow / total;
// E1/E2/E3缩放
while (true) {
if (high <= HALF) {
low = low * 2;
high = high * 2;
value = value * 2;
if (bitPos < bitStream.length()) {
value |= (bitStream.charAt(bitPos++) - '0');
}
} else if (low >= HALF) {
low = (low - HALF) * 2;
high = (high - HALF) * 2;
value = (value - HALF) * 2;
if (bitPos < bitStream.length()) {
value |= (bitStream.charAt(bitPos++) - '0');
}
} else if (low >= QUARTER && high <= 3 * QUARTER) {
low = (low - QUARTER) * 2;
high = (high - QUARTER) * 2;
value = (value - QUARTER) * 2;
if (bitPos < bitStream.length()) {
value |= (bitStream.charAt(bitPos++) - '0');
}
} else {
break;
}
}
}
return result;
}
private void appendPendingBits(StringBuilder sb, long count, char bit) {
for (int i = 0; i < count; i++) {
sb.append(bit);
}
}
private byte[] bitsToBytes(String bits) {
int byteLength = (bits.length() + 7) / 8;
byte[] result = new byte[byteLength];
for (int i = 0; i < bits.length(); i++) {
if (bits.charAt(i) == '1') {
result[i / 8] |= (1 << (7 - (i % 8)));
}
}
return result;
}
private String bytesToBits(byte[] data) {
StringBuilder sb = new StringBuilder();
for (byte b : data) {
for (int i = 7; i >= 0; i--) {
sb.append((b >> i) & 1);
}
}
return sb.toString();
}
/**
* 简化版:仅用于演示核心逻辑的浮点实现
*/
public String encodeSimplified(String input) {
// 统计频率
int[] freq = new int[256];
for (char c : input.toCharArray()) {
freq[c]++;
}
double low = 0.0;
double high = 1.0;
for (char c : input.toCharArray()) {
double range = high - low;
// 计算累积分布
double cumProb = 0.0;
double charLow = 0.0;
double charHigh = 0.0;
for (int i = 0; i < 256; i++) {
double p = (double) freq[i] / input.length();
if (i == c) {
charLow = cumProb;
charHigh = cumProb + p;
}
cumProb += p;
}
high = low + range * charHigh;
low = low + range * charLow;
}
return String.format("区间: [%.20f, %.20f), 中点: %.20f", low, high, (low + high) / 2);
}
public static void main(String[] args) {
ArithmeticCoding ac = new ArithmeticCoding();
// 简化版演示
System.out.println("=== 简化版算术编码演示 ===");
String simpleText = "AAB";
System.out.println("输入: " + simpleText);
System.out.println(ac.encodeSimplified(simpleText));
// 完整编码演示
System.out.println("\n=== 完整算术编码测试 ===");
String testData = "ARITHMETIC_ENCODING_IS_EFFICIENT";
byte[] input = testData.getBytes();
System.out.println("原始数据: " + testData);
System.out.println("原始长度: " + input.length + " 字节 = " + (input.length * 8) + " 位");
EncodedResult encoded = ac.encode(input);
System.out.println("编码后长度: " + encoded.data.length + " 字节 = " + (encoded.data.length * 8) + " 位");
double ratio = 1.0 - (double) (encoded.data.length * 8) / (input.length * 8);
System.out.printf("压缩率: %.2f%%\n", ratio * 100);
// 计算信息熵
double entropy = calculateEntropy(input);
System.out.printf("信息熵理论下限: %.2f 位/符号\n", entropy);
System.out.printf("实际编码: %.2f 位/符号\n", (double) (encoded.data.length * 8) / input.length);
// 解码验证
byte[] decoded = ac.decode(encoded, input.length);
String decodedStr = new String(decoded);
System.out.println("\n解码结果: " + decodedStr);
System.out.println("数据一致性: " + testData.equals(decodedStr));
}
private static double calculateEntropy(byte[] data) {
int[] freq = new int[256];
for (byte b : data) {
freq[b & 0xFF]++;
}
double entropy = 0.0;
for (int f : freq) {
if (f > 0) {
double p = (double) f / data.length;
entropy -= p * (Math.log(p) / Math.log(2));
}
}
return entropy;
}
}
算法原理深入
为什么算术编码优于哈夫曼?
哈夫曼编码为每个符号分配整数位。如果某个符号概率为0.4,其信息量为-log₂(0.4) ≈ 1.32位,但哈夫曼只能分配1位或2位。算术编码通过将多个符号一起编码,可以实现更精确的概率分配。
精度问题与整数实现
浮点实现存在精度溢出问题。实际应用中:
– 使用整数运算模拟[0, 2^N)区间
– 当区间缩小到无法区分符号时,输出已确定的位并重置区间
– 上述代码中的E1/E2/E3缩放就是这一机制
自适应模型
编码过程中动态更新频率表,可以更好地适应数据特征变化。解码端需要同步更新相同的模型。
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 编码时间 | O(n × σ) | n为数据长度,σ为符号集大小 |
| 解码时间 | O(n × σ) | 与编码对称 |
| 空间复杂度 | O(σ) | 频率表存储 |
| 压缩率 | 接近熵限 | 理论上最优的无损压缩 |
实际应用
算术编码是以下标准的核心组件:
– JPEG 2000图像:使用自适应算术编码替代哈夫曼
– H.264/H.265视频:CABAC(上下文自适应二进制算术编码)
– Bzip2:最后的编码阶段使用算术编码
– WebP图像:可选的算术编码模式
总结
算术编码代表了熵编码的理论极限:
1. 最优压缩率:无限接近香农熵
2. 灵活性高:支持静态和自适应概率模型
3. 实现复杂:需要处理精度问题和区间缩放
理解算术编码是掌握现代多媒体压缩标准(JPEG 2000、H.265)的必经之路。