我对Java不太了解,所以我想我的解决方案必须是通用的:)
1. 压缩列表
由于布尔值效率低下,每个都List<Boolean>
应该压缩成一个List<Byte>
,这很容易,一次抓住它们 8 个。
最后一个“字节”可能不完整,因此您当然需要存储已编码的位数。
2. 序列化元素列表
您有两种方法可以继续:要么对列表的项目数进行编码,要么使用模式来标记结束。我建议对项目的数量进行编码,模式方法需要转义并且令人毛骨悚然,而且打包位更难。
要编码长度,您可以使用可变方案:即编码长度所需的字节数应该与长度成正比,我已经使用过。您可以通过在第一个字节上使用前缀来指示用于对长度本身进行编码的字节数:
0... .... > this byte encodes the number of items (7 bits of effective)
10.. .... / .... .... > 2 bytes
110. .... / .... .... / .... .... > 3 bytes
它非常节省空间,并且解码发生在整个字节上,所以不太难。有人可能会说它与 UTF8 方案非常相似 :)
3.递归应用
List< List< Boolean > >
变成[Length Item ... Item]
每个Item
本身就是一个代表的地方List<Boolean>
4. 邮编
我想有一个zlib
可用于 Java 的库,或者其他类似的库deflate
或lcw
. 将它传递给您的缓冲区,并确保尽可能精确地进行压缩,无论花费多少时间。
如果您的表示中有任何重复的模式(即使是您没有看到的),它应该能够压缩它。不过不要盲目相信它,请检查“压缩”形式是否比“未压缩”形式轻,但并非总是如此。
5. 例子
有人注意到跟踪列表的边缘是占用空间的:)
// Tricky here, we indicate how many bits are used, but they are packed into bytes ;)
List<Boolean> list = [false,false,true,true,false,false,true,true]
encode(list) == [0x08, 0x33] // [00001000, 00110011] (2 bytes)
// Easier: the length actually indicates the number of elements
List<List<Boolean>> super = [list,list]
encode(super) == [0x02, 0x08, 0x33, 0x08, 0x33] // [00000010, ...] (5 bytes)
6. 空间消耗
假设我们有一个布尔值,编码它所消耗的空间是List<Boolean>
:n
booleans = ceil( n / 8 )
为了编码比特数(n
),我们需要:
length = 1 for 0 <= n < 2^7 ~ 128
length = 2 for 2^7 <= n < 2^14 ~ 16384
length = 3 for 2^14 <= n < 2^21 ~ 2097152
...
length = ceil( log(n) / 7 ) # for n != 0 ;)
因此要完全编码一个列表:
bytes =
if n == 0: 1
else : ceil( log(n) / 7 ) + ceil( n / 8 )
7. 小清单
但是有一个极端情况:频谱的低端(即几乎是空列表)。
因为n == 1
,bytes
被评估为2
,这确实看起来很浪费。然而,我不会试图猜测一旦压缩开始会发生什么。
您可能希望打包更多。如果我们放弃保留整个字节的想法,这是可能的......
- 保持长度编码不变(在整个字节上),但不要“填充”
List<Boolean>
. 一个元素列表变为0000 0001 x
(9 位)
- 尝试“打包”长度编码
第二点更难,我们有效地归结为双倍长度编码:
- 表示多少位编码长度
- 实际上对这些位的长度进行编码
例如:
0 -> 0 0
1 -> 0 1
2 -> 10 10
3 -> 10 11
4 -> 110 100
5 -> 110 101
8 -> 1110 1000
16 -> 11110 10000 (=> 1 byte and 2 bits)
它适用于非常小的列表,但很快就会退化:
# Original scheme
length = ceil( ( log(n) / 7)
# New scheme
length = 2 * ceil( log(n) )
突破点?8
是的,你没看错,只有少于 8 个元素的列表才更好……而且只有“位”更好。
n -> bits spared
[0,1] -> 6
[2,3] -> 4
[4,7] -> 2
[8,15] -> 0 # Turn point
[16,31] -> -2
[32,63] -> -4
[64,127] -> -6
[128,255] -> 0 # Interesting eh ? That's the whole byte effect!
当然,一旦压缩开始,很可能它并不重要。
我了解您可能会喜欢recursive
的算法,但我仍然建议计算实际空间消耗的数字,或者甚至更好地通过在真实测试集上应用归档来实际测试它。
8.递归/变量编码
我感兴趣地阅读了TheDon
答案,以及他提交给Elias Omega Coding的链接。
在理论领域,它们是合理的答案。不幸的是,它们非常不实用。主要问题是它们具有非常有趣的渐近行为,但是我们什么时候真正需要编码千兆字节的数据呢?很少,如果有的话。
最近一项关于工作中内存使用的研究表明,大多数容器用于十几个(或几十个)项目。只有在一些非常罕见的情况下,我们才能达到一千。当然,对于您的特定问题,最好的方法是实际检查您自己的数据并查看值的分布,但根据经验,我会说您不能只专注于频谱的高端,因为您的数据位于低端.
算法的一个例子TheDon
。说我有一个清单[0,1,0,1,0,1,0,1]
len('01010101') = 8 -> 1000
len('1000') = 4 -> 100
len('100') = 3 -> 11
len('11') = 2 -> 10
encode('01010101') = '10' '0' '11' '0' '100' '0' '1000' '1' '01010101'
len(encode('01010101')) = 2 + 1 + 2 + 1 + 3 + 1 + 4 + 1 + 8 = 23
让我们做一个小表,用各种“阈值”来停止递归。它表示不同范围的开销的位数n
。
threshold 2 3 4 5 My proposal
-----------------------------------------------
[0,3] -> 3 4 5 6 8
[4,7] -> 10 4 5 6 8
[8,15] -> 15 9 5 6 8
[16,31] -> 16 10 5 6 8
[32,63] -> 17 11 12 6 8
[64,127] -> 18 12 13 14 8
[128,255]-> 19 13 14 15 16
公平地说,我专注于低端,我的建议适合这项任务。我想强调的是,它并不是那么明确。特别是因为 near 1
,log
函数几乎是线性的,因此递归失去了它的魅力。门槛帮助很大,3
似乎是一个很好的候选人......
至于Elias omega coding
,就更惨了。来自维基百科文章:
17 -> '10 100 10001 0'
就是这样,一个惊人的 11 位。
道德:您不能在不考虑手头数据的情况下选择编码方案。
所以,除非你List<Boolean>
有数百个长度,否则不要打扰并坚持我的小建议。