我们描述了一种块排序无损数据压缩算法及其实现,并在相同硬件上比较了该实现与广泛使用的数据压缩器的性能。
We describe a block-sorting, lossless data compression algorithm, and our implementation of that algorithm. We compare the performance of our implementation with widely available data compressors running on the same hardware.
该算法通过对输入文本块施加可逆变换来工作。该变换本身不压缩数据,而是重新排列数据,使其更容易用简单算法(如移至前端编码)进行压缩。
The algorithm works by applying a reversible transformation to a block of input text. The transformation does not itself compress the data, but reorders it to make it easy to compress with simple algorithms such as move-to-front coding.
我们的算法在速度上与基于 Lempel-Ziv 技术的算法相当,但获得的压缩率接近最佳的统计建模技术。输入块的大小必须足够大(几千字节)才能获得良好的压缩效果。
Our algorithm achieves speed comparable to algorithms based on the techniques of Lempel and Ziv, but obtains compression close to the best statistical modelling techniques. The size of the input block must be large (a few kilobytes) to achieve good compression.
BWT 的核心理念是「先变换再压缩」——它本身不是一个压缩算法,而是一种可逆的数据重排列预处理步骤。块排序(block-sorting)将字符按照上下文聚集在一起,使得同一字符在变换后的 L 列中彼此靠近。这种聚集效应使得后续简单的局部自适应压缩算法(如 MTF + Huffman)能够取得接近复杂统计建模的压缩率,同时保持 LZ 系列的速度优势。BWT 开创性地将「无损压缩」解耦为两个独立阶段:可逆变换(暴露冗余)和前端压缩(消除冗余)。这一思想后来成为 BWA 基因组比对工具及 FM-index 的基础。
最广泛使用的数据压缩算法基于 Lempel 和 Ziv 的序列数据压缩器 [1, 2]。统计建模技术可能产生更优的压缩效果 [3],但速度明显更慢。
The most widely used data compression algorithms are based on the sequential data compressors of Lempel and Ziv [1, 2]. Statistical modelling techniques may produce superior compression [3], but are significantly slower.
本文提出了一种技术,其压缩率与统计建模技术相差约一个百分点以内,但速度与基于 Lempel-Ziv 的算法相当。
In this paper, we present a technique that achieves compression within a percent or so of that achieved by statistical modelling techniques, but at speeds comparable to those of algorithms based on Lempel and Ziv's.
我们的算法不是按顺序处理输入,而是将文本块作为一个整体单位处理。其思想是:对文本块施加一个可逆变换,生成一个包含相同字符但更容易被简单压缩算法压缩的新块。这种变换倾向于将字符聚集在一起,使得找到某个字符靠近相同字符的另一个实例的概率显著增加。这类文本可以用快速局部自适应算法轻松压缩,例如移至前端编码 [4] 结合 Huffman 或算术编码。
Our algorithm does not process its input sequentially, but instead processes a block of text as a single unit. The idea is to apply a reversible transformation to a block of text to form a new block that contains the same characters, but is easier to compress by simple compression algorithms. The transformation tends to group characters together so that the probability of finding a character close to another instance of the same character is increased substantially. Text of this kind can easily be compressed with fast locally-adaptive algorithms, such as move-to-front coding [4] in combination with Huffman or arithmetic coding.
简言之,我们的算法通过以下方式变换一个包含 N 个字符的字符串 S:构造 S 的 N 个旋转(循环移位),按字典序对它们排序,然后提取每个旋转的最后一个字符。由这些字符构成字符串 L,其中 L 的第 i 个字符是第 i 个排序旋转的最后一个字符。除了 L 之外,算法还计算出原始字符串 S 在排序旋转列表中的索引 I。令人惊讶的是,仅凭 L 和 I 就存在一个高效的算法来恢复原始字符串 S。
Briefly, our algorithm transforms a string S of N characters by forming the N rotations (cyclic shifts) of S, sorting them lexicographically, and extracting the last character of each of the rotations. A string L is formed from these characters, where the ith character of L is the last character of the ith sorted rotation. In addition to L, the algorithm computes the index I of the original string S in the sorted list of rotations. Surprisingly, there is an efficient algorithm to compute the original string S given only L and I.
排序操作将具有相同起始字符的旋转聚集在一起。由于旋转的起始字符与末尾字符相邻,因此 L 中的连续字符在 S 中与相似的字符串相邻。如果字符的上下文是该字符的良好预测器,那么 L 将很容易通过简单的局部自适应压缩算法进行压缩。
The sorting operation brings together rotations with the same initial characters. Since the initial characters of the rotations are adjacent to the final characters, consecutive characters in L are adjacent to similar strings in S. If the context of a character is a good predictor for the character, L will be easy to compress with a simple locally-adaptive compression algorithm.
BWT 论文实际描绘了数据压缩的三条技术路线:(1)LZ 系列——基于滑动窗口/字典的序列压缩,速度最快但压缩率适中(代表:LZ77/LZ78 → LZW → gzip/deflate);(2)统计建模——基于概率上下文模型的压缩(如 PPM),压缩率最高但速度慢、内存开销大;(3)BWT 路线(块排序)——作为中间路线,先整块 BWT 变换再配合简单前端编码,速度接近 LZ、压缩率接近统计建模。BWT 的关键突破在于「块级别」而非「顺序」处理:通过对整个块排序所有循环移位,将每个字符的上下文信息编码进排列结构,使相同上下文的字符在 L 列中聚集,从而将全局上下文建模的高成本转化为一次性排序操作。这种设计后来意外地催生了 FM-index——在对上下文敏感的场景(如 DNA 序列比对)中,BWT 的重排特性使字符串匹配可以仅通过 L 和辅助数组高效完成,远超纯 LZ 或统计建模的应用边界。
本节描述两个子算法。算法 C 执行压缩前对文本块施加的可逆变换,算法 D 执行逆操作。后续章节将介绍压缩变换块的方法。
This section describes two sub-algorithms. Algorithm C performs the reversible transformation that we apply to a block of text before compressing it, and Algorithm D performs the inverse operation. A later section suggests a method for compressing the transformed block of text.
在下文中,我们将字符串视为元素为字符的向量。
In the description below, we treat strings as vectors whose elements are characters.
该算法的输入是一个由 N 个字符 S[0], …, S[N-1] 组成的字符串 S,这些字符选自有序字母表 X。为便于说明,我们给出一个运行示例:字符串 S = 'abraca',N = 6,字母表 X = {'a','b','c','r'}。
This algorithm takes as input a string S of N characters S[0]; …; S[N − 1] selected from an ordered alphabet X of characters. To illustrate the technique, we also give a running example, using the string S = 'abraca', N = 6, and the alphabet X = {'a','b','c','r'}.
构造一个概念上的 N×N 矩阵 M,其元素为字符,行是 S 的旋转(循环移位),并按字典序排序。矩阵 M 中至少有一行包含原始字符串 S。令 I 为第一个这样的行的索引(从 0 开始编号)。
C1. [sort rotations] Form a conceptual N × N matrix M whose elements are characters, and whose rows are the rotations (cyclic shifts) of S, sorted in lexicographical order. At least one of the rows of M contains the original string S. Let I be the index of the first such row, numbering from zero.
在我们的示例中,索引 I = 1,矩阵 M 为:
In our example, the index I = 1 and the matrix M is:
row 0: aabrac
row 1: abraca
row 2: acaabr
row 3: bracaa
row 4: caabra
row 5: racaab
令字符串 L 为 M 的最后一列,其字符为 L[0], …, L[N-1](即 M[0,N-1], …, M[N-1,N-1])。变换的输出是 (L, I)。
C2. [find last characters of rotations] Let the string L be the last column of M, with characters L[0]; …; L[N − 1] (equal to M[0, N−1]; …; M[N−1, N−1]). The output of the transformation is the pair (L, I).
在我们的示例中,L = 'caraab',I = 1(来自步骤 C1)。
In our example, L = 'caraab' and I = 1 (from step C1).
我们使用算法 C 中介绍的示例和符号。算法 D 使用算法 C 的输出 (L, I) 重建其输入——长度为 N 的字符串 S。
We use the example and notation introduced in Algorithm C. Algorithm D uses the output (L, I) of Algorithm C to reconstruct its input, the string S of length N.
这一步计算算法 C 中矩阵 M 的第一列 F。通过对 L 中的字符排序得到 F。注意 M 的任何一列都是原始字符串 S 的一个排列,因此 L 和 F 都是 S 的排列,因而也是彼此的排列。此外,由于 M 的行已排序且 F 是 M 的第一列,F 中的字符也是排序的。
D1. [find first characters of rotations] This step calculates the first column F of the matrix M of Algorithm C. This is done by sorting the characters of L to form F. We observe that any column of the matrix M is a permutation of the original string S. Thus, L and F are both permutations of S, and therefore of one another. Furthermore, because the rows of M are sorted, and F is the first column of M, the characters in F are also sorted.
在我们的示例中,F = 'aaabcr'。
In our example, F = 'aaabcr'.
为便于解释,我们在矩阵 M 的内容上下文中描述此步骤。读者应记住,解压缩器无法获得完整的矩阵;此步骤仅需字符串 F、L 和索引 I(来自输入)。
To assist our explanation, we describe this step in terms of the contents of the matrix M. The reader should remember that the complete matrix is not available to the decompressor; only the strings F, L, and the index I (from the input) are needed by this step.
考虑矩阵 M 中以某个给定字符 ch 开头的行。算法 C 确保矩阵 M 的行按字典序排序,因此以 ch 开头的行按字典序排序。
Consider the rows of the matrix M that start with some given character ch. Algorithm C ensured that the rows of matrix M are sorted lexicographically, so the rows that start with ch are ordered lexicographically.
定义矩阵 M'——将 M 的每一行右移一个字符得到,即对每个 i = 0,…, N-1 和每个 j = 0,…, N-1,M'[i,j] = M[i, (j-1) mod N]。
We define the matrix M' formed by rotating each row of M one character to the right, so for each i = 0,…, N−1, and each j = 0,…, N−1, M'[i,j] = M[i, (j−1) mod N].
在我们的示例中,M 和 M' 为:
In our example, M and M' are:
row 0: M = aabrac M' = caabra
row 1: M = abraca M' = aabrac
row 2: M = acaabr M' = racaab
row 3: M = bracaa M' = abraca
row 4: M = caabra M' = acaabr
row 5: M = racaab M' = bracaa
与 M 一样,M' 的每一行都是 S 的旋转,且 M 的每一行在 M' 中都有一个对应行。我们构造 M' 的方式使得 M' 的行按从第二个字符开始的字典序排序。因此,对于任意给定的字符 ch,M 中以 ch 开头的行与 M' 中以 ch 开头的行出现顺序相同。
Like M, each row of M' is a rotation of S, and for each row of M there is a corresponding row in M'. We constructed M' from M so that the rows of M' are sorted lexicographically starting with their second character. So, if we consider only those rows in M' that start with a character ch, they must appear in lexicographical order relative to one another; they have been sorted lexicographically starting with their second characters, and their first characters are all the same and so do not affect the sort order. Therefore, for any given character ch, the rows in M that begin with ch appear in the same order as the rows in M' that begin with ch.
在我们的示例中,这一点可以用于以 'a' 开头的行来验证。行 'aabrac'、'abraca' 和 'acaabr' 是 M 中的第 0、1、2 行,分别对应 M' 中的第 1、3、4 行。
In our example, this is demonstrated by the rows that begin with 'a'. The rows 'aabrac', 'abraca', and 'acaabr' are rows 0, 1, 2 in M and correspond to rows 1, 3, 4 in M'.
利用 F 和 L(M 和 M' 的第一列),我们计算向量 T,表示两个矩阵行之间的对应关系:对每个 j = 0,…, N-1,M' 的第 j 行对应于 M 的第 T[j] 行。
Using F and L, the first columns of M and M' respectively, we calculate a vector T that indicates the correspondence between the rows of the two matrices, in the sense that for each j = 0,…, N−1, row j of M' corresponds to row T[j] of M.
如果 L[j] 是 ch 在 L 中的第 k 次出现,则 T[j] = i,其中 F[i] 是 ch 在 F 中的第 k 次出现。注意 T 表示 F 元素与 L 元素之间的一一对应,且 F[T[j]] = L[j]。
If L[j] is the kth instance of ch in L, then T[j] = i where F[i] is the kth instance of ch in F. Note that T represents a one-to-one correspondence between elements of F and elements of L, and F[T[j]] = L[j].
在我们的示例中,T = (4 0 5 1 2 3)。
In our example, T is: (4 0 5 1 2 3).
现在,对每个 i = 0,…, N-1,字符 L[i] 和 F[i] 是 M 第 i 行的最后一个和第一个字符。由于每一行都是 S 的一个旋转,字符 L[i] 在 S 中循环前于 F[i]。从 T 的构造可得 F[T[j]] = L[j]。代入 i = T[j] 得 L[T[j]] 在 S 中循环前于 L[j]。
Now, for each i = 0,…, N−1, the characters L[i] and F[i] are the last and first characters of the row i of M. Since each row is a rotation of S, the character L[i] cyclically precedes the character F[i] in S. From the construction of T, we have F[T[j]] = L[j]. Substituting i = T[j], we have L[T[j]] cyclically precedes L[j] in S.
索引 I 由算法 C 定义,使得 M 的第 I 行是 S。因此,S 的最后一个字符是 L[I]。我们使用向量 T 给出每个字符的前驱:对每个 i = 0,…, N-1:S[N-1-i] = L[Ti[I]],其中 T0[x] = x,且 Ti+1[x] = T[Ti[x]]。
The index I is defined by Algorithm C such that row I of M is S. Thus, the last character of S is L[I]. We use the vector T to give the predecessors of each character: for each i = 0,…, N−1: S[N−1−i] = L[Ti[I]], where T0[x] = x, and Ti+1[x] = T[Ti[x]].
这样就得到了原始输入字符串 S。
This yields S, the original input to the compressor.
在我们的示例中,S = 'abraca'。
In our example, S = 'abraca'.
我们本可以定义 T 使得字符串 S 从前向后生成,而非从后向前。上述描述与第 4.2 节中的伪代码一致。
We could have defined T so that the string S would be generated from front to back, rather than the other way around. The description above matches the pseudo-code given in Section 4.2.
序列 Ti[I](i = 0,…, N-1)不一定构成数字 0,…, N-1 的一个排列。如果原始字符串 S 的形式为 Zp(对于某个子串 Z 和某个 p > 1),那么序列 Ti[I] 也将为 Z'p 的形式(对于某个子序列 Z')。也就是说,S 中的重复将通过反复访问 T 的相同元素而生成。例如,如果 S = 'cancan',Z = 'can' 且 p = 2,那么序列 Ti[I] 将为 [2, 4, 0, 2, 4, 0]。
The sequence Ti[I] for i = 0,…, N−1 is not necessarily a permutation of the numbers 0,…, N−1. If the original string S is of the form Zp for some substring Z and some p > 1, then the sequence Ti[I] for i = 0,…, N−1 will also be of the form Z'p for some subsequence Z'. That is, the repetitions in S will be generated by visiting the same elements of T repeatedly. For example, if S = 'cancan', Z = 'can' and p = 2, the sequence Ti[I] for i = 0,…, N−1 will be [2, 4, 0, 2, 4, 0].
一、LF-mapping 与向量 T 的关系
算法 D 中定义的向量 T 是现代 BWT 文献中「LF-mapping」(Last-to-First mapping)的早期形式。给定 L 和 F 两列,LF-mapping 将 L 中第 k 个出现的字符 ch 映射到 F 中第 k 个出现的相同字符 ch。向量 T[j] = i 即表示 L 中位置 j 的字符对应到 F 中位置 i 的同一个字符。T 的实质就是 LF-mapping 的向量表示。
二、算法 C / D 的数学本质
BWT 正向变换的数学本质可归纳为三步: (1) 构造所有循环移位(等价于在字符串末尾加终止符后的所有后缀,但有细微差别); (2) 按字典序排序这些旋转; (3) 提取排序矩阵的最后一列 L 和原始串的行号 I。
逆变换的本质是利用字典序排序带来的性质——同一字符在 F 和 L 中出现的相对顺序是相同的(因为对于以同一字符开头的行,其排序顺序取决于后续字符)。这一性质允许我们仅凭 L 和 I,通过 T 向量(LF-mapping)逐个恢复 S 中的字符。
三、T 向量构造算法(手工操作)
以 S = 'abraca' 为例:
四、逆变换追踪(从后向前)
给定 L = 'caraab',I = 1,T = (4 0 5 1 2 3):
得 S = 'abraca'。注意这是从后向前构建 S 的。
五、重复串的循环行走
论文特别指出序列 Ti[I] 不一定是排列的完整遍历——当 S 包含重复子串时(如 S = Zp),LF 行走会在部分位置上循环,仅覆盖 p 个位置。这一观察对于后续高效实现(第 5 节)中的分块解压缩策略非常重要。
六、与现代 BWT 的差异
现代 BWT 通常使用后缀数组(suffix array)实现,且通过添加唯一终止符 $ 来保证排序的确定性。论文的原始描述(1994 年)使用的是旋转矩阵 M 的全貌,这在概念上更直观但实现上不可行(需要 O(N²) 空间)。第 5 节将展示如何通过后缀排序将复杂度降至 O(N log N)。
七、BWT 的可逆性证明直觉
可逆性的关键在于:排序操作将同一上下文的旋转聚集在一起。F 列(排序后)和 L 列(排序后旋转的最后一个字符)通过「相同字符出现顺序保持一致」这一性质相互关联。这是一个非平凡的性质——正是它使得仅凭 L 和 I 即可唯一恢复原始字符串 S,无需存储完整的旋转矩阵。
算法 C 对输入字符串 S 的旋转进行排序,生成由每个旋转的最后一个字符组成的字符串 L。
要理解为什么这有助于压缩,考虑一段英文文本中一个常见单词中的单个字母。以 'the' 中的字母 't' 为例,假设输入字符串包含许多 'the' 实例。
当输入的旋转列表被排序后,所有以 "he " 开头的旋转都排在一起;它们中很大一部分很可能以 't' 结尾。因此,L 串的一个区域将包含异常大量的 't' 字符,夹杂着在英语中可以出现在 "he " 之前的其他字符,如空格、's'、'T' 和 'S'。
同样的论点可应用于所有单词中的所有字符,因此 L 串的任何局部区域都可能包含大量的少数几个不同的字符。总体效果是,如果字符 ch 在 L 中靠近某点出现,那么它在同一附近再次出现的概率非常高,否则很低。
这种性质正是移至前端编码器 [4] 所需要的——它将一个字符 ch 的实例编码为自上一个 ch 出现以来所看到的不同字符的计数。应用到 L 串时,移至前端编码器的输出将以小数字为主,可以被高效的熵编码器进一步压缩。
Algorithm C sorts the rotations of an input string S, and generates the string L consisting of the last character of each rotation.
To see why this might lead to effective compression, consider the effect on a single letter in a common word in a block of English text. We will use the example of the letter 't' in the word 'the', and assume an input string containing many instances of 'the'.
When the list of rotations of the input is sorted, all the rotations starting with 'he ' will sort together; a large proportion of them are likely to end in 't'. One region of the string L will therefore contain a disproportionately large number of 't' characters, intermingled with other characters that can precede 'he ' in English, such as space, 's', 'T', and 'S'.
The same argument can be applied to all characters in all words, so any localized region of the string L is likely to contain a large number of a few distinct characters. The overall effect is that the probability that given character ch will occur at a given point in L is very high if ch occurs near that point in L, and is low otherwise.
This property is exactly the one needed for effective compression by a move-to-front coder [4], which encodes an instance of character ch by the count of distinct characters seen since the next previous occurrence of ch. When applied to the string L, the output of a move-to-front coder will be dominated by low numbers, which can be efficiently compressed by an entropy coder.
BWT 的核心效应是让字母根据"上下文"聚集。以 'the' 为例:
聚集效应带来两种对压缩有益的性质:
这种"先变换后压缩"的思路在当时很新颖,后来人们发现 BWT 不仅能帮压缩,还能做索引(FM-index),那才是它在生物信息学领域大放异彩的开始。
算法 C 的输出 (L, I) 需要经过进一步编码才能实现压缩。论文使用移至前端(MTF)编码接着熵编码。
将 L 中的每个字符通过移至前端技术编码为整数 R[i]。初始化一个列表 Y,包含字母表 X 中的每个字符恰好一次。对于每个 i = 0,…,N-1,将 R[i] 设为字符 L[i] 在列表 Y 中的位置(从 0 开始计数,即它前面有多少个不同的字符),然后将字符 L[i] 移至 Y 的前端。
以 Y = ['a','b','c','r'] 和 L = "caraab" 为例,计算向量 R:
步骤 M2 对 R 的每个元素应用 Huffman 编码或算术编码,将每个元素作为单独的标记进行编码。
从 R 重建 L。首先使用步骤 M2 中编码方案的逆操作对编码流进行解码,得到 N 个整数的向量 R。然后初始化一个列表 Y,包含字母表 X 中的字符,顺序与步骤 M1 相同。对于每个 i = 0,…,N-1,将 L[i] 设为 Y 中位置 R[i] 处的字符(从 0 开始编号),然后将该字符移至 Y 的前端。
Algorithm M: Move-to-front coding
M1. [move-to-front coding] This step encodes each of the characters in L by applying the move-to-front technique to the individual characters. We define a vector of integers R[0]; …; R[N−1], which are the codes for the characters L[0]; …; L[N−1].
Initialize a list Y of characters to contain each character in the alphabet X exactly once. For each i = 0,…, N−1 in turn, set R[i] to the number of characters preceding character L[i] in the list Y, then move character L[i] to the front of Y.
Taking Y = ['a','b','c','r'] initially, and L = 'caraab', we compute the vector R: (2, 1, 3, 1, 0, 3).
M2. [encode] Apply Huffman or arithmetic coding to the elements of R, treating each element as a separate token to be coded.
Algorithm W: Move-to-front decoding
W1. [decode] Decode the coded stream OUT using the inverse of the coding scheme used in step M2. The result is a vector R of N integers.
W2. [inverse move-to-front coding] Initialize a list Y of characters to contain the characters of the alphabet X in the same order as in step M1. For each i = 0,…, N−1 in turn, set L[i] to be the character at position R[i] in list Y (numbering from 0), then move that character to the front of Y.
MTF 编码的思路很直观:刚用过的字符很可能马上再用。把它放在列表最前面,下次编码时只需要小数字。
MTF 为什么配合 BWT 特别有效?BWT 之后,L 串中相同字符倾向于聚集。想象 L = "aaabcr"(3 个 'a' 连在一起):
连续三个 'a' 编码为 0, 0, 0——可以在 Huffman 中用极少的比特表示。更一般地,当 L 中连续出现相同字符时,MTF 输出连续为 0,形成 0 游程。小数字占用的熵极少,而 0 游程还可以用游程编码进一步压缩。
但如果字符分散在各处(没做 BWT 的原始文本),MTF 会输出许多大数字,压缩效果很差。所以 BWT + MTF 是天生一对。
压缩速度最重要的因素是排序输入块旋转所需的时间。直接用快速排序实现虽然额外空间小(每字符一个词),但在最坏情况下性能较差。
更快的实现方式是将旋转排序问题归约为对某个相关字符串的后缀排序。论文提出了三种方案:
步骤 D1 和 D2 可以高效地通过两趟数据扫描和一趟字母表扫描完成,如以下伪代码所示:
for i := 0 to N-1 do P[i] := C[L[i]]; C[L[i]] := C[L[i]] + 1 end; sum := 0; for ch := FIRST(alphabet) to LAST(alphabet) do sum := sum + C[ch]; C[ch] := sum - C[ch]; end; i := I; for j := N-1 downto 0 do S[j] := L[i]; i := P[i] + C[L[i]] end
4.1 Compression
The most important factor in compression speed is the time taken to sort the rotations of the input block. A simple application of quicksort requires little additional space (one word per character), and works fairly well on most input data. However, its worst-case performance is poor.
A faster way to implement Algorithm C is to reduce the problem of sorting the rotations to the problem of sorting the suffixes of a related string.
4.2 Decompression
Steps D1 and D2 can be accomplished efficiently with only two passes over the data, and one pass over the alphabet, as shown in the pseudo-code below. ...
这段伪代码是逆 BWT 的高效实现,不需要构建完整的 T 向量:
关键洞察:将逆 BWT 分解为 O(N) 的两遍扫描(而非反复搜索 T 向量),使得解压速度极快——这在 1994 年是一项重要的工程优化。现代 BWA 等工具中的反向 BWT 仍然使用相同原理。
如果将来移至前端替换为一种将很久未见的编码仅部分移至前端的方案,压缩率可能会有轻微改善。我们尚未在实现中利用这一点。
一个更重要的观察是:向量 R 中某个值的概率在一定程度上取决紧邻的前驱值。在实践中,最重要的效应是 0 倾向于在 R 中连续成群出现。我们可以利用这一效应,将每一串连续的 0 用一个表示游程长度的编码来替代。对紧跟在 0 游程之后的值,可以使用另一组 Huffman 编码表,因为下一个值不可能又是另一个 0 游程。
Compression can improve slightly if move-to-front is replaced by a scheme in which codes that have not been seen in a very long time are moved only part-way to the front. We have not exploited this in our implementations.
Although simple Huffman and arithmetic coders do well in step M2, a more complex coding scheme can do slightly better. This is because the probability of a certain value at a given point in the vector R depends to a certain extent on the immediately preceding value. In practice, the most important effect is that zeroes tend to occur in runs in R. We can take advantage of this effect by representing each run of zeroes by a code indicating the length of the run. A second set of Huffman codes can be used for values immediately following a run of zeroes, since the next value cannot be another run of zeroes.
论文中提到的这些变体思想后来被经典工具 bzip2(1996 年,Julian Seward)采纳并扩展。bzip2 的完整管道是:
"0 游程编码 + 双 Huffman 表"的思路直接来源于本文。第二组 Huffman 表的直觉是:紧跟在 0 游程后的值不可能是 0,因此其概率分布不同,用专门的编码表可以更高效地压缩。论文的"部分移至前端"思想虽然未被 bzip2 采用,但其核心直觉——某些字符比另一些更适合保持在列表前部——与后来的自适应压缩研究一脉相承。
在表 1 中,我们给出了使用本文算法压缩 Calgary 压缩语料库 [7] 的 14 个常用文件的结果。CPU 时间的测量是在 DECstation 5000/200 上进行的,该机器配备 MIPS R3000 处理器,主频 25MHz,64KB 缓存。
表 2 显示了对两个输入在不同块大小下压缩效果的变化。结果表明,即使块大小已经相当大时,压缩效果仍然随着块大小的增加而持续改善。
表 3 将本文实现的算法 C 与其他压缩程序进行了比较。我们的算法在压缩率上显著优于 compress(LZW)和 gzip(LZ77),接近最优的统计建模方法,而速度更接近基于 Lempel-Ziv 的算法。
In Table 1 we give the results of compressing the fourteen commonly used files of the Calgary Compression Corpus [7] with our algorithm. ... The CPU time measurements were made on a DECstation 5000/200, which has a MIPS R3000 processor clocked at 25MHz with a 64 kbyte cache.
In Table 2, we show the variation of compression effectiveness with input block size for two inputs. ... The table shows that compression improves with increasing block size, even when the block size is quite large.
Table 3 compares our implementation of Algorithm C with other compression programmes.
性能数据的核心看点:
25MHz 的 R3000 处理器上 51.1 秒压缩 3MB——用今天的标准看很慢,但在 1994 年是实用的。
我们描述了一种压缩技术,它通过对文本块施加可逆变换,使输入中的冗余性更易于被简单编码方案利用。我们的算法是通用型的,在文本和非文本输入上均表现良好。该变换通过排序将基于上下文的字符聚集在一起;该技术利用了每个字符仅一侧的上下文信息。
要获得良好的压缩效率,需要数千字符的输入块。算法的有效性随着块大小的增加而持续改善,至少在达到数百万字符之前都是如此。
我们的算法实现了与优秀的统计建模器相当的压缩率,而速度更接近基于 Lempel-Ziv 算法的编码器。与 Lempel-Ziv 算法一样,我们的算法解压快于压缩。
7. Conclusions
We have described a compression technique that works by applying a reversible transformation to a block of text to make redundancy in the input more accessible to simple coding schemes. Our algorithm is general-purpose, in that it does well on both text and non-text inputs. The transformation uses sorting to group characters together based on their contexts; this technique makes use of the context on only one side of each character.
To achieve good compression, input blocks of several thousand characters are needed. The effectiveness of the algorithm continues to improve with increasing block size at least up to blocks of several million characters.
Our algorithm achieves compression comparable with good statistical modellers, yet is closer in speed to coders based on the algorithms of Lempel and Ziv. Like Lempel and Ziv's algorithms, our algorithm decompresses faster than it compresses.
1994 年这篇报告发表时主要被视为一个优秀的压缩算法,但其历史意义远超压缩领域:
论文中"只利用了字符一侧的上下文"这一限制,在生物信息学中反而成了优势:参考基因组序列的 BWT 只需构建一次,查询模式串的前缀即可确定匹配的后缀数组区间。这种单向性恰好与 DNA 读段的比对需求吻合。
一篇原本为压缩而写的技术报告,时隔 15 年成为基因组学的基石——这是计算机科学中"基础研究催生意想不到的重大应用"的经典案例。