Burrows-Wheeler压缩,这个革命性的压缩算法性能优于 gzip 和 PKZIP,并且易于实现,也没有专利保护,它是Unix的bzip2压缩的基础。
Burrows-Wheeler 压缩算法包含三部分,依顺序分别是:
1. Burrows-Wheeler 变换。给定一个典型的英文文本,把它变换成另一个文本,其中相同的字母序列相邻出现;
2. 前移编码(Move-to-Front encoding)。给定一个文本,其中相同字母多次相邻出现,把它转换成另一文本,其中特定字母比其它字母出现的频次更高;
3. Huffman 压缩(Huffman Compression)。给定一个文本,其中特定字母比其它字母出现的频次更高,通过给出现频次高的字母以短编码而其它频次低的字母长编码来进行压缩。
第三步 Huffman 压缩才是真正进行了信息的压缩:这一步之所以有效是因为第一步和第二步能确保给出一个文本,其中某些字母出现的频次要比其它字母出现的频次更高。为了恢复一个消息,反序应用逆操作:首先应用 Huffman 展开,然后前移解码,最后是 Burrows-Wheeler 逆变换。
前移编码与解码:前移编码的主要思想是让字母表中的字母保持一个字母序列,不断地从输入消息中读入字母,打印出字母出现在字母序列中的位置,然后把该字母移动到字母表的前面。举一个例子,如果我们有一个6个字母的字母表,初始次序为 A B C D E F, 然后我们对输入 CAAABCCCACCF 进行编码,我们将如下更新字母序列:
Move-to-Front in out
-------------------- ----- -----
A B C D E F C 2
C A B D E F A 1
A C B D E F A 0
A C B D E F A 0
A C B D E F B 2
B A C D E F C 2
C B A D E F C 0
C B A D E F C 0
C B A D E F A 2
A C B D E F C 1
C A B D E F C 0
C A B D E F F 5
F C A B D E
如果在输入中相同的字母依次重复出现,那么输出的值大部分会是小的整数,比如0,1和2. 某些字母的极高的出现频次给 Huffman 编码创造了理想的应用环境。
循环后缀数组:为了有效实现 Burrows-Wheeler 变换,我们将用到一个基础的数据结构————循环后缀数组,它描述了长度为 N 的字符串的 N 循环后缀的排序后的抽象,考虑一个长度为12的字符串"ABRACADABRA!",下面展示了它的12循环后缀和排序后的结果:
i 原始后缀 排序后的后缀 index[i]
-- ------------------------- --------------------------- -------
0 A B R A C A D A B R A ! ! A B R A C A D A B R A 11
1 B R A C A D A B R A ! A A ! A B R A C A D A B A 10
2 R A C A D A B R A ! A B A B R A ! A B R A C A D 7
3 A C A D A B R A ! A B R A B R A C A D A B R A ! 0
4 C A D A B R A ! A B R A A C A D A B R A ! A B R 3
5 A D A B R A ! A B R A C A D A B R A ! A B R A C 5
6 D A B R A ! A B R A C A B R A ! A B R A C A D A 8
7 A B R A ! A B R A C A D B R A C A D A B R A ! A 1
8 B R A ! A B R A C A D A C A D A B R A ! A B R A 4
9 R A ! A B R A C A D A B D A B R A ! A B R A C A 6
10 A ! A B R A C A D A B A R A ! A B R A C A D A B 9
11 ! A B R A C A D A B A A R A C A D A B R A ! A B 2
我们定义 index[i] 为原始后缀出现在排序后缀的第 i 个位置,例如,index[11] = 2 表示第2个原始后缀出现在第11个排序后缀。
Burrows-Wheeler 变换: Burrows-Wheeler 变换的目的不是为了压缩,而是把消息转换成更易于压缩的形式。该变换重排输入中的字母,以便出现更多的重复字母的聚集,同时保证仍能恢复出原始输入。它依赖以下启示:如果你在英文文本中看到 "hen",大部分情况下它的前一个字母会是't'或'w',如果我们能够把所有前缀的字母集中起来(大部分是't'和一些'w'),那么数据压缩将会变得更简单。
Burrows-Wheeler 编码:一个长度为 N 的字符串 s 的 Burrows-Wheeler 变换定义:考虑 s 的 N 循环后缀的排序结果,Burrows-Wheeler 变换就是原始序列在排序后缀中出现的序号,加上排序后缀数组 t[] 的最后一列。我们依然沿用之前的例子 "ABRACADABRA!",下面的表格将强调出 Burrows-Wheeler 变换。
i 原始后缀 排序后的后缀 t index[i]
---- --------------------------- ------------------------- ---------
0 A B R A C A D A B R A ! ! A B R A C A D A B R A 11
1 B R A C A D A B R A ! A A ! A B R A C A D A B R 10
2 R A C A D A B R A ! A B A B R A ! A B R A C A D 7
*3 A C A D A B R A ! A B R A B R A C A D A B R A ! *0
4 C A D A B R A ! A B R A A C A D A B R A ! A B R 3
5 A D A B R A ! A B R A C A D A B R A ! A B R A C 5
6 D A B R A ! A B R A C A B R A ! A B R A C A D A 8
7 A B R A ! A B R A C A D B R A C A D A B R A ! A 1
8 B R A ! A B R A C A D A C A D A B R A ! A B R A 4
9 R A ! A B R A C A D A B D A B R A ! A B R A C A 6
10 A ! A B R A C A D A B R R A ! A B R A C A D A B 9
11 ! A B R A C A D A B R A R A C A D A B R A ! A B 2
因为原始字符串为"ABRACADABRA!"在第3行,Burrows-Wheeler 变换为:
3
ARD!RCAAAABB
Burrows-Wheeler 解码:现在来描述如何进行Burrows-Wheeler 逆变换以恢复原始输入字串。如果第 j 个原始后缀(原始字串左移了 j 个字母)排序后在第 i 行,我们定义 next[i] 为第(j+1)个原始后缀在排序后的行号。例如,如果 first 为原始输入字串出现的行号,next[first] 就是排序后第一个原始后缀(原始输入字串左移一位后的字串)的行号,next[next[first]] 就是排序后第二个原始后缀的行号,next[next[next[first]]] 是第三个原始后缀的行号,等等。
给定 t[],first,和 next[]数组。Burrows-Wheeler 解码器的输入是排序后缀的最后一列 t[]。从 t[]出发,我们能推出排序后缀的第一列,因为它与最后一列拥有相同的字符,只不过它经过了排序。
i 排序后的后缀 t next
-- -------------------------- -------
0 ! ? ? ? ? ? ? ? ? ? ? A 3
1 A ? ? ? ? ? ? ? ? ? ? R 0
2 A ? ? ? ? ? ? ? ? ? ? D 6
*3 A ? ? ? ? ? ? ? ? ? ? ! 7
4 A ? ? ? ? ? ? ? ? ? ? R 8
5 A ? ? ? ? ? ? ? ? ? ? C 9
6 B ? ? ? ? ? ? ? ? ? ? A 10
7 B ? ? ? ? ? ? ? ? ? ? A 11
8 C ? ? ? ? ? ? ? ? ? ? A 5
9 D ? ? ? ? ? ? ? ? ? ? A 2
10 R ? ? ? ? ? ? ? ? ? ? B 1
11 R ? ? ? ? ? ? ? ? ? ? B 4
- 解码:结定了 next[] 数组和 first,我们能重建出原始输入字串,因为第 i 个原始后缀的第一个字母就是输入字串的第 i 个字母。在上面的中,因为 first = 3,于是我们知道原始输入字串出现在第3行;这原始字串就是以'A'开始(并且以'!'结束)。因为 next[first] = 7,下一个原始后缀出现在第7行,这样原始字串中的下一个字母就是'B',因为 next[next[first]] = 11,下一个原始后缀出现在第11行,于是原始输入字串的下一个字母就是'R'。
- 构造 next[]数组:从first 和 t[] 出发构建 next[]数组。令人吃惊的是,Burrows-Wheeler 变换中含有的信息足以用来构建 next[]数组,由此就能重建原始消息了!具体是这样的:我们可以简单推出在输入字串只出现一次的字母对应的 next[]值。例如,考虑以字母 'C' 开始的后缀,通过观察第一列,它出现在排序后的第8行,它的下一个原始后缀会以'C' 结束,通过观察最后一列,下一个原始后缀出现在排序后的第5行。这样,next[8] = 5,同样地,'D'和'!'也只出现一次,于是我们可能推出 next[9] = 2,next[0] = 3。
i 排序后的后缀 t next
-- --------------------------- -------
0 ! ? ? ? ? ? ? ? ? ? ? A 3
1 A ? ? ? ? ? ? ? ? ? ? R
2 A ? ? ? ? ? ? ? ? ? ? D
*3 A ? ? ? ? ? ? ? ? ? ? !
4 A ? ? ? ? ? ? ? ? ? ? R
5 A ? ? ? ? ? ? ? ? ? ? C
6 B ? ? ? ? ? ? ? ? ? ? A
7 B ? ? ? ? ? ? ? ? ? ? A
8 C ? ? ? ? ? ? ? ? ? ? A 5
9 D ? ? ? ? ? ? ? ? ? ? A 2
10 R ? ? ? ? ? ? ? ? ? ? B
11 R ? ? ? ? ? ? ? ? ? ? B
然而,因为 'R' 出现了两次,我们就不能确定到底是 next[10] = 1, next[11] = 4 还是 next[10] = 4, next[11] = 1。这儿有一个关键规则来消除这个不确定性:
- 如果排序后的第 i 和第 j 行都以相同的字母为起始并且 i < j, 那么 next[i] < next[j].
这个规则指出 next[10] = 1, next [11] = 4. 这条规则为什么有效呢?因为这些行都是排序过的,所以第10行以字典序小于第11行。于是,在第10行中的10个未知字母必须小于第11行中的10个未知字母(因为第10行和第11行都是以'R'开始的),我们同样知道在以'R'结束的两行之间,第1行比第4行小。但是,第10行和第11行中的10个未知字母就是第1行和第4行的前10个字母。这样,next[10] = 1, next[11] = 4 不然这与后缀是经过排序这一事实相矛盾。
至此,本文讲解了如何进行 Burrows-Wheeler 变换,前移编码。Huffman 编码可没有进行详细讲解,读者可以从其他参考源获知如何进行 Huffman编码。