有一种非常简单且相当有效的压缩技术,可用于已知范围内的排序整数。与大多数压缩方案一样,它针对串行访问进行了优化,但您可以根据需要构建索引来加速随机访问。
这是一种增量编码(即每个数字由与前一个数字的距离表示),由一个编码向量组成
例如,如果 k 为 4,则序列:
00011 1 1 00000 1 00001
编码三个数字。第一个四位编码 (3) 是第一个 delta,取自初始值 0,因此第一个数字是 3。接下来的两个单独的 1 累加为 2·24 , 或 32,将其添加到以下 0000 的增量中,总共为 32。所以第二个数字是 3+32=35。最后最后一个delta是单个24加1,一共17个,第三个数是35+17=52。
1 位表示下一个 delta 应该增加 2k(或者,更一般地说,每个 delta 增加 2k 倍立即数前 1 位。)
另一种可能更好的想法是将每个增量编码为可变长度的位序列:1i0(1|0)k,表示 i·2k+[k 位后缀]的增量。但第一个演示文稿更符合最优性证明。
由于每个“1”码代表2k的增量,所以不能超过m/2k个,其中m是最大的数设置为压缩。其余代码均对应数字,总长度为 n·(k + 1),其中 n 为集合的大小。 k 的最佳值大致为 log2 m/n,在您的情况下为 7 或 8。
我对算法的概念进行了快速验证,而无需担心优化问题。它仍然很快;对随机样本进行排序比压缩/解压缩它需要更长的时间。我尝试了一些不同的种子和向量大小,从 16,400,000 到 31,000,000,值范围为 [0, 4,000,000,000)。每个数据值使用的位范围从 8.59 (n=31000000) 到 9.45 (n=16400000)。所有的测试都是用 7 位后缀完成的; log2 m/n 从 7.01 (n=31000000) 到 7.93 (n=16400000) 不等。我尝试使用 6 位和 8 位后缀;除了在 n=31000000 的情况下,6 位后缀略小,7 位后缀总是最好的。所以我猜想最优 k 不完全是 floor(log2 m/n) 但离它不远了。
压缩代码:
void Compress(std::ostream& os,
const std::vector<unsigned long>& v,
unsigned long k = 0) {
BitOut out(os);
out.put(v.size(), 64);
if (v.size()) {
unsigned long twok;
if (k == 0) {
unsigned long ratio = v.back() / v.size();
for (twok = 1; twok <= ratio / 2; ++k, twok *= 2) { }
} else {
twok = 1 << k;
}
out.put(k, 32);
unsigned long prev = 0;
for (unsigned long val : v) {
while (val - prev >= twok) { out.put(1); prev += twok; }
out.put(0);
out.put(val - prev, k);
prev = val;
}
}
out.flush(1);
}
解压:
std::vector<unsigned long> Decompress(std::istream& is) {
BitIn in(is);
unsigned long size = in.get(64);
if (size) {
unsigned long k = in.get(32);
unsigned long twok = 1 << k;
std::vector<unsigned long> v;
v.reserve(size);
unsigned long prev = 0;
for (; size; --size) {
while (in.get()) prev += twok;
prev += in.get(k);
v.push_back(prev);
}
}
return v;
}
使用可变长度编码可能有点尴尬;另一种方法是将每个代码(1 或 0)的第一位存储在一个位向量中,并将 k 位后缀存储在一个单独的向量中。如果 k 为 8,这将特别方便。
导致文件稍长但更容易为其构建索引的变体是仅使用 1 位作为增量。那么对于某个 a,可能为 0,delta 始终为 a·2k,其中 a 是后缀代码之前的连续 1 位的数量。然后,该索引由位向量中每个第 Nth 个 1 位的位置以及后缀向量中的相应索引(即与位向量中的下一个 0 对应的后缀的索引)组成)。