男人沉默代表什么:算术编码 + 统计模型 = 数据压缩 - 第一部分:算术编码(http://compres...

来源:百度文库 编辑:偶看新闻 时间:2024/04/28 23:20:04
现在通用的大多数数据压缩方法都属于两大阵营之一:基于字典的方案统计方法。 在小系统世界中,基于字典的数据压缩技术此时似乎更加流行。不过,通过将算术编码与强大的模型技术结合在一起,数据压缩的统计方法可以真正达到更好的性能。这篇分成两部分的文章讨论了如何用几个不同的模型方法与算术编码组合以达到一些重大的压缩率。本文的第一部分详细说明算术编码是如何工作的。第二部分说明如何开发一些可以使用算术编码的有效模型以生成高性能压缩程序。 钟爱的术语       数据压缩通常通过从输入“文本”获取“符号”、处理它们,并将“代码”写入到压缩后的文件来进行运作。对于本文来说,符号通常是字节,但是他们很可能只是像素、80 位的浮点数或者 EBCDIC 字符。数据压缩方案需要能够将已压缩的文件转换回到与输入文本的一样的拷贝才是有效的。如果已压缩的文件比输入文本更小,那么不必说,它也是有用的。       基于字典的压缩系统通过用固定长度码来代替输入文本中的一组符号来进行运作。字典技术的一个众所周知的例子是 LZW 数据压缩。(请参见 DDJ 的 89 年第 10 期中的“LZW 数据压缩”一文)。LZW 通过通常从 9 到 16 位大小范围的码来取代本来无限长的字符串来进行运作。       数据压缩的统计方法采取一种完全不同的方法。它们通过一次编码多个符号来运作。将符号编码到可变长的输出码中。输出码的长度根据符号的概率或者频率进行变化。低概率的符用较多的位进行编码,并且高概率符用较少的位进行编码。        实践中,统计和字典方法之间的分界线并不总是那么清晰。一些方案并不能明显地归为某一个阵营或者另一个,并且总是有一些使用来自两种技术特性的混合方案。不过,在本文中讨论的方法使用算术编码来实现纯粹的统计压缩方案。 霍夫曼(Huffman)编码:退役的冠军          在数据流中只是能够精确地计算字符的概率还不够,我们也需要能有效地利用这个知识的编码方法。基于概率统计的最著名的编码方法可能是霍夫曼编码。D.A.霍夫曼在 1952 年发表了一篇论文说明为已给定其概率的一组符号创建码表的方法。当使用固定长度的编码时,霍夫曼编码表保证产生最低可能的输出位来统计输入流中可能的符号。霍夫曼称这些“最小冗余编码”,但是这个方案现在统称为霍夫曼编码。其它固定长度编码系统,如香农-范诺(Shannon-Fano)编码,通过霍夫曼证明不是最理想的。       霍夫曼编码为每个符号指定一个输出码,输出码可以 1 位这么短,也可以比输入符号长得多,严格取决于它们的概率。用于每个符号的优化位数是以 2 为底,(1/p)的对数,这里 p 是指定字符的概率。因此,例如,如果可以在随机字节流中找到的字符概率是 1/256,每个字符的最佳位数是以 2 为底的 256 的对数,或者 8。如果概率上升到 1/2,编码字符所需要的最佳位数将降为 1。       这个方案放到现实中时的问题是霍夫曼编码的位长度必须是整数。例如,如果字符的概率是 1/3,编码这个字符的最佳位数是大约 1.6。霍夫曼编码方案必须要么指定 1 位,要么指定 2 位给编码,并且任何一种选择都导致可能比理论更长的压缩消息。       当字符的概率变得很高时,这个非最佳编码也变成值得注意的问题。如果能开发一种可以为给定的字符指定 90% 的概率的统计方法的话,最佳编码码大小将是 0.15 个比特位。霍夫曼编码系统只可能给符号指定一个 1 位的码,这要比必需的码位长 6 倍。 自适应方案       当尝试进行自适应数据压缩时产生的霍夫曼编码的第二个问题。当进行非自适应数据压缩时,压缩程序单程扫描一遍数据来收集统计信息。然后使用在整个编码过程中都不能改变的统计信息来编码数据。       解码程序为了解码已压缩的数据流,它首先需要一个统计信息的副本。编码程序一般情况下将预先考虑的统计表放入已压缩的消息中,以使解码程序可以在开始之前读入统计表。这显然给消息增加了一定量的负担。       当使用很简单的统计模型压缩数据时,编码表趋于更小。例如,每个字符的频率计数能以相当高的精确性存储在差不多 256 个字节大小的空间中。它并不会给除真正最小的消息之外的其它任何消息增加明显的长度。不过,为了获得更好的压缩率,统计模型在大小上有必要的增加。如果消息的统计变得太大,在压缩率中的任何改进被将需要预先放入已编码消息中的统计而增加的长度抵消了。       为了回避这个问题,提出了自适应数据压缩。在自适应数据压缩中,编码程序和解码程序都以其处于相同状态的统计模型开始。两者中的每一个一次处理单个字符,并在字符读入后更新统计模型。这非常类似于大多数像 LZW 编码这样基于字典方案的工作。以非最佳模型开始的消息会有少量的效率损失,但是它经常通过不必随消息传送任何统计信息而得到更好的弥补。       将自适应模型与霍夫曼编码结合起来的问题是重建霍夫曼树是一个非常昂贵的过程。为了让自适应方案高效,有必要在每一个字符出现之后调整霍夫曼编码。直到霍夫曼编码第一次开发的 20 年之后,执行自适应霍夫曼编码的算法都没有发布。即使在现在,最好的自适应霍夫曼编码算法仍然相当耗时间和金钱。 算术编码(Arithmetic Coding):它如何工作       一个替代霍夫曼编码的可观候选方案已在仅近十年得到证实:算术编码(Arithmetic Coding)。算术编码完全放弃了用指定的编码取代输入符号的主张。取而代之的是,它接受输入符号流并用单个浮点输出数来代替它。消息越长(并且越复杂),在输出数中就需要越多的位。直到最近仍然没有发现在计算机上用固定大小的寄存器来实现这一思想的实践方法。       算术编码过程产生的输出是单个小于 1 并大于等于 0 的数。这个单个的数可以唯一地解码以创建参与其构造的精确字符流。为了构造输出数,符号的编码必须有一组指定给它们的概率。例如,如果我要编码随机消息“BILL GATES”,我会得到看起来如下的概率分布: Character                            Probability--------------                           --------------  SPACE                                 1/10     A                                     1/10     B                                     1/10     E                                     1/10     G                                     1/10     I                                      1/10     L                                      2/10     S                                      1/10     T                                      1/10      一旦知道了字符的概率,需要按“概率线”为单个符号指定一个范围,这个“概率线”名义上是从 0 到 1。为哪个字符指定哪一段范围并不麻烦,只要编码程序和解码程序都采用相同的方式来指定范围。我们在这里使用的这一组九个字符符号将看起来如下: Character                            Probability                     Range---------------                          ----------------                  ---------------- SPACE                                   1/10                        0.00 - 0.10     A                                      1/10                        0.10 - 0.20     B                                      1/10                        0.20 - 0.30     E                                      1/10                        0.30 - 0.40     G                                      1/10                        0.40 - 0.50     I                                       1/10                        0.50 - 0.60     L                                       2/10                       0.60 - 0.80     S                                       1/10                       0.80 - 0.90    T                                        1/10                       0.90 - 1.00       在 0 - 1 范围内为每一个字符指定一个与其出现的概率对应的的片断。当编码消息“BILL GATES”时,第一个符号是“B”。要正确编码第一个字符,最终编码后的消息必须是大于或者等于 0.20 并小于 0.30 的一个数。要编码这个数,我们所要做的是明了这个数会落入的范围。因此,在编码完第一个字符后,这个范围的最底端是 0.20,并且范围的最顶端是 0.30。       在编码完第一个字符之后,我们知道对于我们的输出数来说我们的范围现在就以这个底数和这个顶数为边界。在剩余的编码过程期间发生的事情是每一个要编码的新符号将更进一步地限制输出数的可能范围。下一个要编码的字符,“I”,拥有范围 0.50 到 0.60。如果它是我们消息中的第一个数,我们可以直接将我们的底和顶的范围设为这两个值。但是“I”是第二个数。因此我们不能直接设定,而是应该说“I”在 0.2 - 0.3  的子范围中拥有与 0.50 - 0.60 对应的范围。这意味着新编码的数将必然落入当前已建立范围的第 50 至第 60 个百分点之间的位置。按照这个逻辑将我们的数进一步限制到 0.25 至 0.26 之间的范围。       对任意长度的消息按照这种思路完成编码的算法说明如下:       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 + range*high_range(symbol)        low = low + range*low_range(symbol)      End of While      output low       按照这个流程可以得出对我们所选择的消息的自然而简单的总结,看起来如下: New Character                   Low Value                       High Value--------------------                   ---------------                      ---------------                                       0.0                                1.0       B                              0.2                                0.3       I                               0.25                              0.26       L                              0.256                             0.258       L                              0.2572                           0.2576   SPACE                          0.25720                          0.25724      G                              0.257216                        0.257220      A                              0.2572164                      0.2572168      T                              0.25721676                    0.2572168      E                              0.257216772                  0.257216776      S                              0.2572167752                0.2572167756       因此最终的底值,0.2572167752 将使用我们介绍的编码方案唯一编码消息“BILL GATES”。       有了这个编码方案,明白解码过程如何运作相对比较容易。我们通过看哪一个符号拥有我们已编码消息所落在的码空间来查找到第一个符号。因为数 0.2572167752 落在 0.2 和 0.3 之间,我们知道第一个字符必然是“B”。然后我们需要将“B”从已编码的数中移除。因为我们知道 B 的底和顶范围,我们可以用与将它们放入过程相反的过程来消除它们的影响。首先,我们从数中减去 B 的底值,得到 0.0572167752,然后我们除以 B 的范围,即 0.1。所得的值为 0.572167752。然后我们可以计算出在哪里停止,这个停止的位置正是在下一个字母“I”的范围之内。       解码输入数的算法看上去如下:       get encoded number      Do        find symbol whose range straddles the encoded number        output the symbol        range = symbol low value - symbol high value        subtract symbol low value from encoded number        divide encoded number by range      until no more symbols           注意我为了方便起见忽略了判断什么时候才没有更多的符号需要解码的问题。可以通过将一个特殊 EOF 符号编码进消息,也可以随已编码的消息一同传送流的长度来解决这个问题。        “BILL GATES”消息的解码算法的处理过程如下: Encoded Number     Output Symbol     Low     High     Range-----------------------      --------------------     ------     ------     ---------0.2572167752                  B              0.2       0.3       0.10.5721677752                  I               0.5       0.6       0.10.72167752                     L               0.6       0.8       0.20.6083876                       L               0.6       0.8       0.20.041938                     SPACE           0.0       0.1       0.10.41938                          G               0.4       0.5       0.10.1938                            A               0.2       0.3       0.10.938                              T               0.9       1.0       0.10.38                                E               0.3       0.4       0.10.8                                  S               0.8       0.9       0.10.0                       概括来说,编码过程就是一个简单地使用每一个新的符号来收缩数的可能范围这样一个过程。新的范围与附加给这个符号的预先定义的概率成比例。解码是逆向过程,其中展开的范围与抽取出的每一个字符的概率成比例。 实践中的问题       使用算术编码编/解码符号流不是太复杂。但是初看上去,它似乎完全无法实现。大多数计算机最多支持的 80 位或差不多的浮点数。这是不是就意味着你必须在每次完成 10 到 15 个符号以后重新开始?你需要浮点处理器?不同浮点格式的机器可以使用算术编码来通信吗?       如它所展现的,算术编码最好使用标准 16 位和 32 位整数来完成。不必非要要求浮点数,也不必要求其帮助才能使用算术编码。代替浮点的方法是增量转换方案,在方案中,声明为定点整数的变量接收在底端的新位,并将这些位转移到顶端,最终形成一个大的单数,这个数的位只取决于计算机的存储媒介,媒介能存储多长,这个数就有多大。       在前面的部分,我通过明了括起可能输出数的范围的顶端数和底端数,来说明算法如何工作。当算法第一次启动时,底端数设为 0.0,并且顶端数设为 1.0。使用整数首先进行的简化是以二进制的形式改变 1.0 至 0.999......,或者.111......。       为了在整数寄存器中存储这些数,我们首先调整它们,以让上面这个隐含的小数点是在词的左边。然后我们载入与适合我们要处理的单词所需要大小差不多一样的初始顶值和底值。我的实现使用 16 位无符号数,因此初始的顶值是 0xFFFF,并且底值是 0。我们知道顶值从 FF 开始无限增长,并且底值从零开始无限增长,因此我们可以在需要这些剩余位(也就是最后两位(在 0x00 与 0xFF 之间))时我们无需再附加顶值和底值,而是直接将它们转移出来。       如果你想像我们的“BILL GATES”例子在一个具有 5 个数的寄存器中,我们设置与上述相当的这个数看起来应当如下:       顶值:99999      底值:00000       为了找到我们新的范围数,我们需要应用前面部分说明的编码算法。我们首先需要计算底值和顶值之间的范围。两个寄存器之间的差值将是 100000,而不是 99999。这是因为我们假设有无穷多个的 9(实际上只受限于我们要处理的单词数量) 被加到了存储顶值的寄存器上,因此我们需要增加计算出来的差值。那么我们使用来自前面部分的方程来计算新的顶值:       high = low + high_range(symbol)            在这种情况下顶值范围是 .30,这个范围给出了一个新的顶值 30000。存储新的顶值之前,我们需要先将它减 1,再次强调,因为有那个隐藏的数字加到了整数值上。因此,新的顶值是 29999。       底端数的计算遵循相同的方法,结果得到新值为 20000。因此,现在顶值和底值看起来如下:       顶端值: 29999 (999...)      底端值: 20000 (000...)       从这一点来说,顶端和底端的最高位的数字应该匹配。因为我们的算法本质,顶端和底端可以继续增长接近至另一个完全没有匹配过的数。这意味着一旦它们在最高位的数字上匹配时,这个数字就不再改变。 因此,我们现在可以输出这个数字作为我们编码数的第一个数字。这可以通过将顶端和底端向左移动一个数字来完成,并且在顶端的最低位数字后移入一个 9 进来。在这个算法的 C 实现中是以二进制的形式执行相当的操作。       随着这个过程的进行,顶端和底端继续向着接近碰头的方向增长,然后将转移出的数字编码进单词中。我们的“BILL GATES”消息的过程看起来如下:                                                      HIGH    LOW    RANGE   CUMULATIVE OUTPUT Initial state                       99999   00000  100000Encode B (0.2 - 0.3)          29999   20000Shift out 2                        99999   00000 100000  .2Encode I (0.5 - 0.6)           59999   50000              .2Shift out 5                        99999   00000 100000  .25Encode L (0.6 - 0.8)          79999   60000  20000   .25Encode L (0.6 - 0.8)          75999   72000              .25Shift out 7                       59999    20000  40000   .257Encode SPACE (0.0 - 0.1)  23999    20000              .257Shift out 2                       39999    00000  40000   .2572Encode G (0.4 - 0.5)         19999    16000              .2572Shift out 1                       99999    60000  40000   .25721Encode A (0.1 - 0.2)         67999    64000              .25721Shift out 6                       79999    40000  40000   .257216Encode T (0.9 - 1.0)         79999    76000              .257216Shift out 7                       99999    60000  40000   .2572167Encode E (0.3 - 0.4)         75999    72000              .2572167Shift out 7                       59999    20000  40000   .25721677Encode S (0.8 - 0.9)         55999    52000              .25721677Shift out 5                       59999    20000              .257216775Shift out 2                                                           .2572167752Shift out 0                                                           .25721677520        注意,在所有的字母都解决完后,两个额外的数字需要转移出顶端值或者底端值来结束输出单词。 复杂性       这个方案对于增量编码一条消息来说非常适合。在使用双精度整数计算以确保消息可以精确地编码的期间,精确度是有保证的。不过,在某些情形下,精确度会有一定潜在的损失。       如果已编码的词其中有 0 个或 9 个字符串,顶端值和底端值将慢慢地会聚到一个值,但是可能不是立即看它们的最高位数字的匹配。例如,顶值和底值可能看起来如下:       High: 700004      Low: 699995       此时,计算的范围的长度将只是一个单个数字,这意味我们会没有足够的精度来精确地编码这个输出词。甚至更糟,在几次更多的迭代后,顶端数和底端数可能会看起来如下:       High: 70000      Low: 69999       此时,这两个值永远也粘不到一块了。顶端值和底端值变得如此小,以至于任何计算总是返回相同的值。但是,因为两个词的最高位的数字并不相等,算法并不能输出数字并进行转移。它似乎更像是一个僵局。       战胜这个下溢问题的办法是防止事情变得这么糟。最初的算法说过一些像“如果顶值和底值的最高位的数字匹配的话,将它转移出来”这样的事情。如果两个数字不匹配,而它们现在是很邻近的数,就需要应用第二次测试。如果顶端数和底端数还是分开的(也就是还没有碰上),那么我们测试看顶端中第二高位的数字是否是 0,以及底端第二高位的数字是否是 9。如果是,它意味着我们正在接近下溢,并且需要采取行动。       当下溢出现,我们采用一个些微转移操作来阻止它。不是将最高位的数字转移出这个词,而是从顶端值和底端值中删除第二高位的数字,并将剩余的数字左移以填充空白。最高位的数字仍留在原位。然后我们需要调置一个下溢计数器来记住我们扔掉了一个数字,并且我们不能完全确信它是否就会是以 0 或者 9 结束。操作看起来如下:                       Before              After                      ---------              --------High:               40344              43449Low:               39810              38100Underflow:       0                     1        在每一次重新计算操作之后,如果最高位的数字并不能匹配上,我们可以再次检查下溢数字。如果下溢仍然出现,我们将它们转移出来并增加计算机器。       当最高位的数字最终会聚到一个单数值时,我们首先输出这个值。然后,我们输出所有在前面丢弃的“下溢”数字。下溢数字将全部是 9 或者全部是 0,取决于顶端数和底端数是否会聚到一个较高的值还是一个较低的值。在这个算法的 C 实现中,下溢计数明了输出了多少个 9 或者 0。 解码       在“理想的”解码过程中,我们使用整个输入数进行工作。因此算法要求我们做一些像“通过符号概率划分已编码的数”。在实践中,我们不能在一个可能有十亿个字节那么长的数上执行操作。正如编码过程,解确程序通过将 16 和 32 位整数用于计算来运作。        取代维护两个数,顶端数和底端数,解码程序必须维护 3 个整数。前两个是顶端数,严格对应到由编码程序维护的顶端数和底端数。第三个数,编码,包含正在从输入位流中读入的当前的位。编码值将总是处于顶端值和底端值之间。像它们越来越接近一样,将发生新的转移操作,并且顶端数和底端数将从编码退出。       在解码程序中,顶端值和底端值严格对应于编码程序所使用的顶端值和底端值。这两个值正如它们在编码程序中一样,在每一个符号之后都将被更新,并且应该有精确的相同值。通过在顶端数和底端数的第一个的数字上执行相同的比较测试,解码程序知道何时是将一个新的数字转移到输入编码中的时间。也与编码程序步调一致,执行相同的下溢测试。       在理想的算法中,通过查找其概率接近编码的当前值的符号来判断当前已编码的符号是什么是可行的。在整数算法中,事情些微更复杂一些。在这种情况下,概率缩放因子由顶端数和底端数之间的差来决定。因此将使用两个 16 位的正整数之间来计算范围,来代替 0.1 和 1.0 之间的范围计算。当前的概率由落入这个范围的当前编码所决定。如果你用(high-low+1)除(value-low),你会得到当前符号的实际概率。 哪儿有牛肉?       到目前为止,这个编码过程可能看起来很有意思,但是为什么说它在霍夫曼编码的基础上有所改进,看起来并不显然。当我们测试一个概率有一点不同的案例时,答案就变得很清晰了。让我们测试一个我们必须编码流“AAAAAAA”的案例,并且“A”的概率已知是 0.90。这意味着任何输入字符有 90% 的机会是字母“A”。我们建立起我们的概率表,以使字母“A”占有范围 0.0 到 0.9,并且消息符号的结束占有 0.9 到 1.0 的范围。那么编码过程看起来如下: New Character          Low value          High value--------------------          --------------          --------------                              0.0                   1.0        A                    0.0                   0.9        A                    0.0                   0.81        A                    0.0                   0.729        A                    0.0                   0.6561        A                    0.0                   0.59049        A                    0.0                   0.531441        A                    0.0                   0.4782969        END                0.43046721       0.4782969       现在我们知道底端值和顶端值是什么,剩下的所有事情就是拿一个数来编码这个消息。数“0.45”将可以使这条消息唯一解码成“AAAAAAA”。这两个数字用了比 7 位还些微少点空间来定义,这意味着我们用少于 8 位的空间编码了 8 个符号。最佳的霍夫曼使用这个方案来编码上面的消息最少也要 9 位。      进一步延伸这个观点,我设置一个只定义了两个符号的测试。字节值“0”有 16382/16383 的概率,并且 EOF 符号有 1/16383 的出现概率。然后我创建一个测试文件其中填上一百万(100,000)个“0”。使用这个模型压缩之后,输出文件只有 3 个字节长!使用霍夫曼编码的最小的长度也有 12,501 个字节。这显然是一个人为的例子,但是它论证了当符号的概率正确的时候,算术编码能够以比每个字节只占 1 位还好的比率压缩数据。 编码       一个用 C 编写的编码过程放在列表1 和 2 中。它包含两个部分,一个初始化过程,和编码程序本身。用于编码程序以及解码程序的编码第一次由 IanH.Witten、Radford Neal 和 John Cleary 发表在“Communication of the ACM”的 1987 年 2 月期的一篇名为“Arithmetic Coding for Data Compression”的论文中,并且经过作者的允许在此公开。我稍微修改了这个编码以使程序的建模部分和编码部分进一步地隔离开。       先前说明的两个算法之间有两个主要的差异以及代码入在列表 1-2。第一个不同是在转换概率的方法中。在上面说明的算法中,概率保持为一对范围为 0.0 至 1.0 的浮点数。每一个符号有其自己的范围部分。在我们在这里说明的程序中,符号有稍微不同的定义。符号的范围定义为两个经过缩放因子计算得出的整数而不是两个浮点数。这个缩放因子也作为符号定义的一部分包括其中。因此,对于“BILL GATES”的例子,字母 L 在前面定义为一对值为 0.60 和 0.80 的顶/底端数。在此处使用的编码中,用值为 10 的确良符号缩放因子,计算出底和顶端数分别为 6 和 8 来定义“B”。采用这种方法来完成定义的原因是因为它在建模这种类型的数据时,能很好地与我们保持统计的办法相适应。这两种方法是相当的,只是保持用整数进行运算减少了许多需要完成的工作。注意字符实际上一直拥有范围,但不包括顶端值。       在这个算法中的第二个不同之处是所有比较和转移操作都是以二进制的形式完成的,而不是十进制。前面给出的说明是基于十进制数字进行的,以使算法更好理解一点。算法在十进制下可以正常工作,但是在大多数计算机上进行十进制数的屏蔽数字和移位很困难。因此我们现在比较最开始的两个位,而不是比较最开始的两个数字。       我们丢失了为了使用编码和解码算法所需要的两件事情。第一件是一组面向位的输入和输出程序。这放在了列表 3 和 4 中,并且在代码中没有注释。建模代码负责跟踪每一个字符的概率,并执行两个不同的转换。在编码过程期间,建模代码取将要被编码的字符并将其转换成概率范围。概率范围定义为一个底数、一个顶数和一个总范围。在解码过程期间,建模代码必须取一个来自输入位流的计数并将其转换成一个字符进行输出。 测试代码       一个短的测试程序放在列表 3 中。它实现一个使用前面讨论的“BILL GATES”模型的压缩/展开程序。在几个需要判断的地方加入了打印语句以让你准确地跟踪在这个程序中进行的处理。       BILL.C 有其自己非常简单的模型单元,这个模型对每一个消息中可能的字母都有一组因定的概率。我加入了一个新字符,即空字符串结束符,以知道何时停止解码这条消息。       BILL.C 编码一个随便定义的输入字符串,并将其写出到一个叫作 TEST.CMP 的文件中。然后它解码这条消息将其打印在屏幕上进行验证。在 BILL.C 中有几个仍然没有讨论过的建模函数。在编码过程期间,调用了一个称为 convert_int_to_symbol 程序。这个程序获取一个给定的输入字符,并将其转换成一个底值、顶值和用在此模型中的缩放因子。因为我们用于 BILL.C 的模型是一组固定的概率,这就意味着在这个表中查询概率。这些一旦定义完毕,就可以调用编码程序。       在解码过程期间,有两个与建模有关的函数。为了决定输入流中什么字符正在等待被解码,模型需要询问模型来判断当前的缩放因子是多少。在我们的例子中,缩放因子(或者是计算范围)是固定在 11,因为在我们的模型中总是有 11 次计算。这个数被传送给解码程序,并且它为使用这个缩放因子的给定符号返回一个计数。然后,调用一个称为 convert_symbol_to_int 的建模函数调用。它获取给定的这个计数,并判断什么字符与这个计数相匹配。最终,再次调用解码程序来解出输入流中的那个字符。 下一次       一旦你成功地理解了如何使用算术编码来编码/解码符号,那么你可以开始尝试创建将具有超级压缩性能的模型。下一个月的总结文章讨论一些你可以尝试的自适应压缩方法。一个相当简单的统计模型程序能够提供超过备受尊敬的程序,如 PKZIP 或者 COMPRESS 的压缩。 参考资源       如我在前面提到,在 1987 年 7 月的 “Communication of the ACM”(ACM 通信)提供权威的算术编码的概论。这篇文章的大部分在由 Timothy C.Bell、John G. Cleary 和 Ian H.Witten 所著的“Text Compression”(文本压缩)一书中重印。这本书为基于统计和基于字典的压缩两种技术提供了出色的概论。另一本好书是由 James Storer 所著的“Data Compression”(数据压缩)。 Bell, Timothy C., Cleary, John G., Witten, Ian H,(1990) "Text Compression",Prentice Hall, Englewood NJ Nelson, Mark(1989) "LZW Data Compression", Doctor Dobb's Journal, October, pp 29-37 Storer, J.A.,(1988) "Data Compression", Computer Science Press, Rockville, MD Witten, Ian H., Neal, Radford M., and Cleary, John G.(1987)"Arithmetic Coding for Data Compression", Communications of the ACM, June, pp 520-540.        列表 1 coder.h 使用算术编码程序所需要的变量、声明以及原型。这些声明用于需要与 coder.c 中的算术编码程序接口所需要的程序。 列表 2 coder.c 这个文件包含了完成符号的算术编码所需要的代码。在这个模块中的所有程序为了完成编码需要知道的是符号计算的概率和缩放因子是什么。这个信息通常入在 SYMBOL 结构中。 列表 3 bitio.h 这个文件包含了使用位流输入/输出程序所需要的函数原型。 列表 4 bitio.c 这个程序包含了一组用于算术数据压缩的面向位的输入输出程序。 列表 5 bill.c 这个小小的验证程序将使用算术数据压缩来编码然后解码只使用了一个由包含在句子“BILL GATES”中的字母组成的字符串。它使用固定、硬编码的概率表