使用 C# 进行算术压缩






4.77/5 (15投票s)
本文描述了算术压缩及其在 C# 中的实现。
引言
根据维基百科
算术编码是一种无损数据压缩方法。通常,像“hello there”这样的字符字符串是用每个字符固定数量的比特来表示的,就像 ASCII 码一样。与霍夫曼编码一样,算术编码是一种可变长度熵编码形式,它将字符串转换为另一种形式,该形式使用更多的比特来表示常用字符,目标是总共使用的比特更少。与其他熵编码技术不同,这些技术将输入消息分解为其组成符号并将每个符号替换为代码字,算术编码将整个消息编码为一个数字,即一个分数 ne,其中 (0.0 ≤ n < 1.0)。
算术编码的一些特性是:
- 当应用于独立同分布 (IID) 源时,每个符号的压缩都是可证明最优的。
- 它在各种情况和压缩比下都非常有效。相同的算术编码实现可以有效地对不同过程(如建模参数、变换系数、信号等)创建的所有多样化数据进行编码。
- 它简化了复杂源的建模,为非 IID 源提供了近乎最优或显著改进的压缩。
- 其主要过程是算术运算,而算术运算得到了所有通用或数字处理器日益提高的效率的支持。
- 它适合那些不是编码专家或不想自己实现编码算法的人作为压缩“黑盒”使用。
在本文中,我将介绍 C# 中算术编码器的实现(源代码中的 *coder.cs*)。该编码算法改编自 Mark Nelson 在其论文中最初用 C 语言提出的算法,该论文可在 http://dogma.net/markn/articles/arith/part1.htm 找到。编码算法已与统计模型分离,并封装为 C# 对象。
描述
本文附带的源代码中的“coder
”对象可以作为算术编码器/解码器的黑盒实现。但是,要使用它,我们需要理解算术编码统计模型的基础以及算术编码的一些基本概念。
通常,使用算术编码取决于创建数据统计模型。在此示例中,我将假设我们正在尝试编码单词“HELLO WORLD”。
创建数据统计模型的过程如下:
- 我们取要编码的单词(HELLO WORLD)中独立字符的数量,按字母顺序排列得到:
- D
- E
- H
- L
- O
- R
- W
- 我们可以将这些字符排列成一个表格,并列出它们对应的频率,如下所示:
- 每个字符将根据其出现频率/概率分配一个范围。此范围将在 0 和 1 之间,如下所示(请注意,我没有对频率和概率模型使用任何优化;要获得最理想的压缩,这是必需的;但是,就我们的示例而言,这应该足够了)。
- 编码算法如下:
- 解码数字并检索编码字符串/数据的算法如下:
要编码的字符总数为 10。为方便起见,我忽略了单词 HELLO 和 WORLD 之间的空格。
字符 | 频率 |
D | 1 |
E | 1 |
H | 1 |
L | 3 |
O | 2 |
R | 1 |
W | 1 |
字符 | 频率 | 概率 | Range |
D | 1 | 1/10 | 0.0 – 0.1 |
E | 1 | 1/10 | 0.1 – 0.2 |
H | 1 | 1/10 | 0.2 – 0.3 |
L | 3 | 3/10 | 0.3 – 0.6 |
O | 2 | 2/10 | 0.6 – 0.8 |
R | 1 | 1/10 | 0.8 – 0.9 |
W | 1 | 1/10 | 0.9 – 1.0 |
Set low to 0.0
Set high to 1.0
While there are still input symbols do
get an input symbol
code_range = high - low.
high = low + code_range * high_range of the symbol being coded
low = low + code_range * low_range of the symbol being coded
End of While
output low
将其应用于我们的输入(HELLO WORLD),我们得到:
encoding H (Hs range is 0.2 - 0.3) Range(or code_range above) = 1 - 0 = 1
low = 0 + (1 * 0.2) = 0.2
high = 0 + (1 * 0.3) = 0.3
no output
encoding E (Es range is 0.1 - 0.2) Range = 0.3 - 0.2 = 0.1
low = 0.2 + (0.1 * 0.1) = 0.21
high = 0.2 + (0.1 * 0.2) = 0.22
output 0.2
encoding L (Ls range is 0.3 - 0.6) Range = 0.22 - 0.21 = 0.01
low = 0.21 + (0.01 * 0.3) = 0.213
high = 0.21 + (0.01 * 0.6) = 0.216
output 0.21
encoding the next L (Ls range is 0.3 - 0.6) Range = 0.216 - 0.213 = 0.003
low = 0.213 + (0.003 * 0.3) = 0.2139
high = 0.213 + (0.003 * 0.6) = 0.2148
no output
encoding O (Os range is 0.6 - 0.8) Range = 0.2148 - 0.2139 = 0.0009
low = 0.2139 + (0.0009 * 0.6) = 0.21444
high = 0.2139 + (0.0009 * 0.8) = 0.21462
output 0.214
encoding W (Ws range is 0.9 - 1.0) Range = 0.21462 - 0.21444 = 0.00018
low = 0.21444 + (0.00018 * 0.9) = 0.214602
high = 0.21444 + (0.00018 * 1.0) = 0.21462
output 0.2146
encoding O (Os range is 0.6 - 0.8) Range = 0.21462 - 0.214602 = 0.000018
low = 0.214602 + (0.000018 * 0.6) = 0.2146128
high = 0.214602 + (0.000018 * 0.8) = 0.2146164
output 0.21461
encoding R (Rs range is 0.8 - 0.9) Range = 0.2146164 - 0.2146128 = 0.0000036
low = 0.2146128 + (0.0000036 * 0.8) = 0.21461568
high = 0.2146128 + (0.0000036 * 0.9) = 0.21461604
no output
encoding L (Ls range is 0.3 - 0.6) Range = 0.21461604 - 0.21461568 = 0.00000036
low = 0.21461568 + (0.00000036 * 0.3) = 0.214615788
high = 0.21461568 + (0.00000036 * 0.6) = 0.214615896
output 0.214615
encoding D (Ds range is 0.0 - 0.1) Range = 0.214615896 - 0.214615788 = 0.000000108
low = 0.214615788 + (0.000000108 * 0.0) = 0.214615788
high = 0.214615788 + (0.000000108 * 0.1) = 0.2146157988
output 0.2146157 and 88 from low
...
得到下表:
字符 | 频率 | 概率 | Range | 低功耗 | 高 |
H | 1 | 1/10 | 0.2 – 0.3 | 0.2 | 0.3 |
E | 1 | 1/10 | 0.1 – 0.2 | 0.21 | 0.22 |
L | 3 | 3/10 | 0.3 – 0.6 | 0.213 | 0.216 |
L | 3 | 3/10 | 0.3 – 0.6 | 0.2139 | 0.2148 |
O | 2 | 3/10 | 0.6 – 0.8 | 0.21444 | 0.21462 |
W | 1 | 1/10 | 0.9 – 1.0 | 0.214602 | 0.214620 |
O | 2 | 2/10 | 0.6 – 0.8 | 0.2146128 | 0.2146164 |
R | 1 | 1/10 | 0.8 – 0.9 | 0.21461568 | 0.21461604 |
L | 3 | 3/10 | 0.3 – 0.6 | 0.214615788 | 0.214615896 |
D | 1 | 1/10 | 0.0 – 0.1 | 0.214615788 | 0.214615806 |
因此,最终的低值 0.214615788 将编码字符串“HELLOWORLD”(不含空格,为清晰起见已省略)。
找到数字所在的范围所代表的符号,输出它。移除编码的影响并重复。伪代码如下:
get the number encoding the data
loop
current symbol = the symbol/character in which range the number falls
current range = current symbols high value – current symbols low value
subtract current symbols low value from number
divide the number by the current range
end loop
该算法将产生如下工作过程,从上面的示例中获取输出:
The number is 0.214615788
current symbol = H (range: 0.2 – 0.3)
current range = 0.3 – 0.2 = 0.1
subtract current symbols low value from number = 0.214615788 – 0.2 = 0.014615788
divide the number by the current range = 0.014615788/0.1 = 0.14615788
current symbol = E (range: 0.1 – 0.2)
current range = 0.2 – 0.1 = 0.1
subtract current symbols low value from number = 0.14615788 – 0.1 = 0.04615788
divide the number by the current range = 0.04615788/0.1 = 0.4615788
current symbol = L (range: 0.3 – 0.6)
current range = 0.6 – 0.3 = 0.3
subtract current symbols low value from number = 0.4615788 – 0.3 = 0.1615788
divide the number by the current range = 0.1615788/0.3 = 0.538596
current symbol = L (range: 0.3 – 0.6)
current range = 0.6 – 0.3 = 0.3
subtract current symbols low value from number = 0.538596 – 0.3 = 0.238596
divide the number by the current range = 0.238596/0.3 = 0.79532
current symbol = O (range: 0.6 – 0.8)
current range = 0.8 – 0.6 = 0.2
subtract current symbols low value from number = 0.79532 – 0.6 = 0.19532
divide the number by the current range = 0.19532/0.2 = 0.9766
current symbol = W (range: 0.9 – 1.0)
current range = 0.9 – 1.0 = 0.1
subtract current symbols low value from number = 0.9766 – 0.9 = 0.0766
divide the number by the current range = 0.0766/0.1 = 0.766
这个过程会得到下表:
字符 | 频率 | 概率 | Range | 数字 |
H | 1 | 1/10 | 0.2 – 0.3 | 0.214615788 |
E | 1 | 1/10 | 0.1 – 0.2 | 0.14615788 |
L | 3 | 3/10 | 0.3 – 0.6 | 0.4615788 |
L | 3 | 3/10 | 0.3 – 0.6 | 0.538596 |
O | 2 | 3/10 | 0.6 – 0.8 | 0.79532 |
W | 1 | 1/10 | 0.9 – 1.0 | 0.9766 |
O | 2 | 2/10 | 0.6 – 0.8 | 0.766 |
R | 1 | 1/10 | 0.8 – 0.9 | 0.83 |
L | 3 | 3/10 | 0.3 – 0.6 | 0.3 |
D | 1 | 1/10 | 0.0 – 0.1 | 0.0 |
实现
上述算法为我们提供了算术编码工作实现的基礎。
在算术编码/解码的实际实现中,请注意:
- 分配给符号的范围包括该范围的下限但不包括上限。换句话说,在我们的示例中,符号“H”拥有的范围是 0.2 到 0.29999... 但不包括 0.3。数学上,这可以写成 H 拥有的范围是 (0.2 ≤ n < 0.3)。
- 我们可以使用整数算术来实现算术编码。这使我们能够简化符号和范围的表示,并摆脱浮点计算的一些限制。我们通过使用整数并在范围开头设置一个虚构的小数点来实现这种简化。使用这种简化并将其应用于我们的示例,我们的概率表现在看起来像这样:
- 考虑上面的编码表:
- 上述计算可以使用整数算术进行建模。如下所述:
- 解码
- 最后,必须有一些机制来停止解码计算,因为理论上,一个编码数字可能会产生比它所编码的更多的解码字符。有两种可能的方法可以做到这一点。可以将一个特殊的
字符编码到数字中,或者,可以将一个代表要编码的字符数量的数字传递给解码函数/方法。
字符 | 频率 | 概率 | Range |
D | 1 | 1/10 | 0000 – 0999 |
E | 1 | 1/10 | 1000 – 1999 |
H | 1 | 1/10 | 2000 – 2999 |
L | 3 | 3/10 | 3000 – 5999 |
O | 2 | 2/10 | 6000 – 7999 |
R | 1 | 1/10 | 8000 – 8999 |
W | 1 | 1/10 | 9000 – 9999 |
基本上,这意味着符号 D 拥有的范围是 0.0000 到 0.0999...,用数学符号表示就是范围 (0.0 ≤ n < 0.1)。
请记住,数字 99999... 应该被视为 0.9999...,小数点后有无穷多个 9。极限情况下,与这个上限 1 之间存在微乎其微的差异。
字符 | 频率 | 概率 | Range | 低功耗 | 高 |
H | 1 | 1/10 | 0.2 – 0.3 | 0.2 | 0.3 |
E | 1 | 1/10 | 0.1 – 0.2 | 0.21 | 0.22 |
L | 3 | 3/10 | 0.3 – 0.6 | 0.213 | 0.216 |
L | 3 | 3/10 | 0.3 – 0.6 | 0.2139 | 0.2148 |
O | 2 | 3/10 | 0.6 – 0.8 | 0.21444 | 0.21462 |
W | 1 | 1/10 | 0.9 – 1.0 | 0.214602 | 0.214620 |
O | 2 | 2/10 | 0.6 – 0.8 | 0.2146128 | 0.2146164 |
R | 1 | 1/10 | 0.8 – 0.9 | 0.21461568 | 0.21461604 |
L | 3 | 3/10 | 0.3 – 0.6 | 0.214615788 | 0.214615896 |
D | 1 | 1/10 | 0.0 – 0.1 | 0.214615788 | 0.214615806 |
请注意,随着编码的进行,高位和低位数字的有效数字趋于收敛。
在第二次迭代(编码 E 时),数字 2 对高位和低位数字都收敛了,并且无论之后编码多少个字符都不会再改变。这是编码算法不断缩小编码范围的一个特性。
随着编码的进行,我们得到低位数字序列 0.2、0.21、0.213,以及高位数字序列 0.3、0.22、0.216(编码 L 时输出)。此时,高位和低位数字的有效数字 2 和 1 已经收敛。
再次,在第五次迭代(编码 O 时),当前低位数字是 0.21444,当前高位数字是 0.21462。此时,最高有效数字 2、1 和 4 已经收敛。
在实际实现中,一旦高位和低位数字的有效数字收敛,就可以认为它们对计算不再有进一步影响。它们的作用是简单地缩小编码范围,保留编码的“小数点位置”,但对后续计算没有进一步的显著影响。
如果我们想象我们的高位和低位数字位于一个无限大的数组(或一个非常大的数组)中,在我们实际计算中,我们可以安全地丢弃任何收敛的有效数字,只需将整个数组向右移动一个“位置”。
我们可以将高位和低位之间的范围设置为 00000 和 99999,并在这些数字前面放一个虚构的小数点。此外,我们将假定小数点后低位数字的 0 的数量无限延伸,高位数字的 9 的数量也无限延伸。
出于计算目的,虽然 00000 (0.00000...) 和 99999 (0.99999...) 之间的范围实际上是 99999 (0.99999...),但我们将其加 1 (0.000...1)。这是因为小数点后的 9 的数量被认为是无限的。因此,极限情况下高位和低位数字之间的差实际上是 1。
请记住,虚构的小数点位于下方每个高位或低位范围数字的前面。数字 0.000000... 和 0.99999... 应该假定为无限延续。由于我们的界限现在是 0.00000 和 0.99999,而不是 0.0 和 1.0,因此在计算范围时,我们将加 1 来补偿。即,高位值应视为 1。
在下面的伪代码中,MSD 代表最高有效数字或数字中最左边的数字。
set low to 00000
set high to 99999
while there are still input symbols
get an input symbol
code_range = (high – low) + 1
high = low + code_range * high_range of the symbol being coded
low = low + code_range * low_range of the symbol being coded
while the MSD of high and low match
if the MSD of the high and low match
output MSD
remove MSD from low and shift a new 0 into low
remove MSD of high and shift a new 9 into high
end if
end while the MSD of high and low match
end while there are still input symbols
将其应用于我们的输入(HELLO WORLD),我们得到:
First initialize high and low,
Set low to 00000 (or 0.000...)
Set high to 99999(or 0.999...)
encoding H (Hs range is 0.2 – 0.3) Range(or code_range above) =
99999 - 00000 = 99990 + 1 = 100000 (or 1.00000...)
low = 00000 + (100000 * 0.2) = 20000 (or 0.200000)
high = 00000 + (100000 * 0.3) = 30000 – 1 = 29999 (or .299999...)
另外,此时 2 已收敛,因此移出 2 并进一步向高位数组移入另一个 9 会得到新的高位和低位,如下所示:
output 2
Set low to 00000
Set high to 99999
encoding E (Es range is 0.1 – 0.2) Range = (99999 – 00000) + 1 = 100000
low = 00000 + (100000 * 0.1) = 10000 (or 0.100000...)
high = 19999 + (100000 * 0.2) = 20000 – 1 = 19999 (or 0.199999....)
此时 1 已收敛,因此移出 1 并进一步向高位数组移入另一个 9 会得到新的高位和低位,如下所示:
output 21
Set low to 00000
Set high to 99999
encoding L (Ls range is 0.3 – 0.6) Range = (99999 – 00000) + 1 = 100000
low = 00000 + (100000 * 0.3) = 30000 (or 0.30000)
high = 00000 + (100000 * 0.6) = 60000 – 1 = 59999 (since 0.5999 = 0.599999...)
no output
Set low to 30000
Set high to 59999
encoding the next L (Ls range is 0.3 – 0.6)
Range= (59999 - 30000) + 1 = 29999 + 1 = 30000
low = 30000 + (30000 * 0.3) = 30000 + 9000 = 39000
high = 30000 + (30000 * 0.6) = 30000 + 18000 = 48000 – 1 = 47999
no output
Set low to 39000
Set high to 47999
encoding O (Os range is 0.6 - 0.8) Range = (47999 – 39000) + 1 = 9000
low = 39000 + (9000 * 0.6) = 39000 + 5400 = 44400
high = 39000 + (9000 * 0.8) = 39000 + 7200 = 46200 – 1 = 46199
此时 4 已收敛,因此移出 4 并进一步向高位和低位数组移入另一个数字会得到新的高位和低位,如下所示:
output 214
Set low to 44000
Set high to 61999
encoding W (Ws range is 0.9 – 1.0) Range = (61999 – 44000) + 1 = 18000
low = 44000 + (18000 * 0.9) = 44000 + 16200 = 60200
high = 44000 + (18000 * 1.0) = 44000 + 18000 = 62000 – 1 = 61999
此时 6 已收敛,因此移出 6 并进一步向高位和低位数组移入另一个数字会得到新的高位和低位,如下所示:
output 2146
Set low to 02000
Set high to 19999
encoding O (Os range is 0.6 – 0.8) Range = (19999 - 02000) + 1 = 18000
low = 2000 + (18000 * 0.6) = 2000 + 10800 = 12800
high = 2000 + (18000 * 0.8) = 2000 + 14400 = 16400 – 1 = 16399
此时 1 已收敛,因此移出它并向高位和低位数组移入另一个数字会得到新的高位和低位,如下所示:
output 21461
Set low to 28000
Set high to 63999
encoding R (Rs range is 0.8 – 0.9) Range = (63999 – 28000) + 1 = 36000
low = 28000 + (36000 * 0.8) = 28000 + 28800 = 56800
high = 28000 + (36000 * 0.9) = 28000 + 32400 = 60400 – 1 = 60399
no output
Set low to 56800
Set high to 60399
encoding L (Ls range is 0.3 – 0.6) Range = (60399 – 56800) + 1 = 3600
low = 56800 + (3600 * 0.3) = 56800 + 1080 = 57880
high = 56800 + (3600 * 0.6) = 56800 + 2160 = 58960 – 1 = 58959
此时 5 已收敛,因此移出它并向高位和低位数组移入另一个数字会得到新的高位和低位,如下所示:
output 214615
Set low to 78800
Set high to 89599
encoding D (Ls range is 0.0 – 0.1) Range = (89599 – 78800) + 1 = 10800
low = 78800 + (1080 * 0.0) = 78800 + 0 = 78800
high = 78800 + (1080 * 0.1) = 78800 + 108 = 78908
此时 7 和 8 已收敛,因此移出 7 和 8 并进一步向输出数组移入另一个数字会得到编码字符串:
output 21461578
output 8 from low
the decimal procedure : output 0.2146157 and 88 from low
END ENCODING
...
得到下表:
字符 | 概率 | Range | 低功耗 | 高 | 输出 |
初始化 | 00000 | 99999 | |||
H | 1/10 | 0.2 – 0.3 | 20000 | 29999 | 2 |
E | 1/10 | 0.1 – 0.2 | 10000 | 19999 | 1 |
L | 3/10 | 0.3 – 0.6 | 30000 | 59999 | |
L | 3/10 | 0.3 – 0.6 | 39000 | 47999 | |
O | 3/10 | 0.6 – 0.8 | 44400 | 46199 | 4 |
W | 1/10 | 0.9 – 1.0 | 60200 | 61999 | 6 |
O | 2/10 | 0.6 – 0.8 | 12800 | 16399 | 1 |
R | 1/10 | 0.8 – 0.9 | 56800 | 60399 | |
L | 3/10 | 0.3 – 0.6 | 57880 | 58959 | 5 |
D | 1/10 | 0.0 – 0.1 | 78800 | 78907 | 78 |
最后,我们也移出低位中的最后一个数字:8。
此逻辑已在 coder
对象/类的方法 encode_symbol
(源代码中的 *coder.cs*)中实现。
下溢
在编码符号时,有可能出现高位和低位无法收敛的情况。如果编码的单词中包含一串 0 或 9,高位和低位值将缓慢收敛到一个值,但其最高有效数字可能不会立即匹配。例如,高位和低位可能看起来像这样:
High: 700003
Low: 699994
计算出的范围只有一位数字长,这意味着编码器没有足够的精度来进行准确计算。
实际上,高位和低位之间的范围变得非常小,以至于任何计算都会返回相同的值。此外,由于高位和低位数字的最高有效数字不相等,算法无法输出数字并移位。
避免下溢的方法是完全预防它。这可以通过稍微修改算法来完成。如果两个 MSD 不匹配,但现在相邻,则进行第二次测试。如果高位和低位相差 1,我们测试高位的第二位数字是否为 0,低位的第二位数字是否为 9。如果是,则表示下溢正在威胁。
当存在下溢的可能性时,编码器会执行略有不同的移位操作。它会删除高位和低位的第二位数字,并将其余数字向左移位。但是,最高有效数字保持不变。此外,下溢计数器被设置为标记被丢弃的数字。该操作如下所示:
Before After
------ -----
High: 40344 43449
Low: 39810 38100
Underflow: 0 1
这会在每次迭代/计算操作后检查是否发生下溢。
当 MSD 最终收敛到单个值时,它会输出该值,然后输出之前丢弃的若干个“下溢”数字。下溢数字将是全 9 或全 0,具体取决于高位和低位收敛到较高值还是较低值。在此算术算法的 C# 实现中,下溢计数器跟踪需要输出多少个 1 或 0。
之前,在解码过程中,我们可以使用整个输入数字。然而,这在实践中可能不可行,因为我们无法对可能达数百万甚至数十亿字节长的数字执行此类操作。就像在编码过程中一样,解码器可以使用简单的有限整数计算进行操作。
解码器维护三个整数。前两个(高位和低位)与编码器维护的高位和低位值完全对应。第三个数字 `code` 包含从输入比特流中读取的当前比特。
重要提示:当前概率由当前 `code` 值落在该范围内的位置决定。如果用 `value-low` 除以 `high-low+1`,就可以得到当前符号的实际概率。
C# 实现
C# 编写的编码过程包含在此文章的下载源代码中。编码器和解码器的代码最早发表(以 C 语言)在 1987 年 2 月的《Communications of the ACM》杂志上发表的一篇题为“Arithmetic Coding for Data Compression”的文章中,作者是 Ian H. Witten、Radford Neal 和 John Cleary,之后由 Mark Nelson 以 C 代码形式再次发表,然后由我自己移植到 C#,并经作者许可在此发布。
我稍微修改了代码,以便进一步分离统计建模和算术编码。编码器(源代码中的 *coder.cs*)类是一个实现算术编码的对象。然后,任何数据的统计模型都可以使用这个类。我包含了测试项目和一些测试该类编码和解码方法的示例。
本文中的算法与本文包含的代码之间存在两个主要区别。
第一个区别在于传输概率的方式。在上述算法中,概率被保留为 0.0 到 1.0 范围内的浮点数对。每个符号都有该范围的自己的部分。在本文包含的 C# 类中,符号的定义略有不同。与两个浮点数不同,符号的范围定义为两个整数,它们是沿着某个标尺的计数。
标尺也作为编码器类定义的一部分包含在内:作为类的属性 'scale
' (coder.scale
)。此标尺在 `encode_symbol` 和 `decode_symbol` 方法中使用,用于将概率整数转换为其浮点等价物,或将编码符号解码为字符等价物。这也意味着类的用户可以输入整数而不是浮点数的符号概率。请参阅包含的测试解决方案以获取示例。
因此,例如在“HELLO WORLD”示例中,字母 H 之前被定义为 0.2 和 0.3 的高/低对。在当前使用的代码中,“H”将被定义为低位和高位计数分别为 2 和 3,符号标尺为 10。
此算法的第二个区别是,所有比较和移位操作都是以 2 为基数进行的,而不是以 10 为基数。之前的插图是用 10 进制数字进行的,以使算法更容易理解。算法在 10 进制下也能正常工作,但在大多数计算机上,以 10 进制进行数字屏蔽和移位成本高且速度慢。我们不再比较两个 MSD 数字,而是比较两个 MSD 位。
为了使用编码和解码算法,还需要两样东西。第一个是面向比特的输入和输出例程。这些在代码列表中显示,并在此作为位 IO 例程呈现。
coder.symbol
结构负责存储每个字符的概率,并执行两种不同的转换。
在编码过程中,coder.symbol
结构需要接受一个要编码的字符,并将其转换为概率范围。概率范围定义为结构中的低位计数和高位计数。
在解码过程中,coder.symbol
结构需要接受一个从输入比特流派生的计数,并将其转换为一个要输出的字符。
coder
对象通过由这些 coder.symbol
结构组成的“字典”或“字母表”创建。
测试代码
包含的解决方案中展示了一个示例程序。它实现了一个压缩/解压缩程序,该程序使用基于所讨论示例的任意模型。
解码方法需要知道编码字符串的长度,以便知道何时停止解码消息。
测试项目编码一个任意定义的输入字符串,并将其写入 MemoryStream
对象。这是该类 `Encode` 方法的返回值。返回的 MemoryStream
对象将包含一个字节数组,其中包含二进制格式的压缩数据。
然后可以通过调用该类的 `Expand` 方法来解码流。该类的 `Expand` 方法接受一个内存流和编码消息的长度作为参数。为了测试它,我们可以编码一个字符串,然后将返回的二进制流传回 `Expand` 方法进行解码。
在编码过程中,会调用一个名为 `convert_int_to_symbol` 的例程。此例程获取给定的输入字符,并使用当前模型将其转换为低计数、高计数和标尺。由于我们的模型是一组固定概率,这仅仅意味着在输入 coder.symbol
结构中查找概率。一旦定义了这些,就可以调用编码器。
在解码过程中,有两个与建模相关的函数。为了确定输入流中等待解码的字符是什么,需要询问模型以确定当前的标尺是什么。在我们的示例中,标尺(或计数范围)由 coder.scale
属性固定。解码时,会调用一个名为 `convert_symbol_to_int` 的建模函数。它接受给定的计数,并确定与该计数匹配的字符。最后,再次调用解码器来处理输入流中的该字符。