Burrows–Wheeler
Princeton Algorithm Assignment Burrows–Wheeler
普林斯顿大学算法课 Burrows–Wheeler
Burrows–Wheeler 算法是一个革命性的压缩算法,可以对 gzip
和 PKZIP
进行压缩,并且构成了 Unix 系统压缩工具 bzip2
的基础,该算法分为 3 个主要的部分:
- Burrows–Wheeler 变换。给定一段英文文本,将其转化为具有如下格式的文本序列:相同的字符会在相邻的位置出现多次。
- Move-to-Front 编码。将 Burrows–Wheeler 变换后的文本编码为特定字符出现的频率比其他字符更高的文本。
- 对上述编码进行 Huffman 压缩。哈夫曼压缩可将出现频率更高的字符用更短的编码值表示,从而实现高效压缩。
事实上,第 3 步的 Huffman 压缩才是唯一对信息进行压缩的步骤,但是前 2 步的变换和编码可以保证特定字符出现的频率高于其他字符,从而保证 Huffman 压缩具有较高的压缩效率。
为了展开压缩后的信息,我们可以将上述操作逆向进行:首先进行 Huffman 解码,然后进行 Move-to-Front 解码,最后逆向进行 Burrows–Wheeler 变换。
Princeton 的 Algorithm 课程作业要求实现 Burrows–Wheeler 变换和 Move-to-Front 编码,并调用课程所学 Huffman 算法完成完整的压缩程序。为了方便代码调试,课程提供了 BinaryStdIn
、BinaryStdOut
以及 HexDump
工具。
Move-to-Front 编码和解码的主要思想是通过反复从输入信息中读取一个字符,打印该字符在序列中出现的位置,并将该字符移动到序列的前面,从而保持字母表中字符的有序序列。例如对于 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
-
对
C
进行加密,发现C
的位置是 2(A
位于 0、B
位于 1、C
位于 2),所以输出结果为 2,接着将C
移动到序列的最前端,此时序列变为C A B D E F
。 -
对
A
进行加密时,A
出现在序列的位置是 1,所以输出结果为 1,并将A
移动到序列最前端,此时序列变为A C B D E F
。 -
继续对
A
进行加密,此时A
出现的位置是 0,所以输出 0,以此类推……
如果在输入中多次出现彼此接近的字符,那么许多输出值将是较小的整数(如 0、1 和 2 等),由此产生的这些字符(较多的 0、1 和 2 等)的频率会很高,提供了 Huffman 编码所能达到的、有利压缩比的输入。例如 CAAABCCCACCF
的编码中出现了 5 次 0、2 次 1 和 4 次 2。
Move-to-Front 编码的任务是依次读入每一个字节(8 个二进制位,看作字符 char
),输出其在序列中的位置,并将其移动到最前面。Move-to-Front 解码的任务是依次读入每一个字节(8 个二进制位,看作 0 - 255 之间的无符号整数),输出这个整数所代表的位置上的字符,并将改字符移动到序列最前面。
这一部分的代码比较简单,只需要使用 BinaryIn 和 BinaryOut 即可,注意运行到最后要 flush()
刷新输入输出缓冲区,并注意输入输出整型时,需要指定只输出或读入 8 位。
LinkedList<Character> sequence = generateInitialSequence();
while (!in.isEmpty()) {
char c = in.readChar();
int index = sequence.indexOf(c);
out.write(index, RELEVANT_BITS);
sequence.remove((Object)c);
sequence.addFirst(c);
}
out.flush(); // out.close();
由于没有 remove(char c)
的函数签名,所以 char c
会被当做 int index
,并执行 remove(int index)
,在 LinkedList 中,这是代表移除第 index
个位置上的元素。我们希望的是移除那个内容为 c
的元素,所以这里强制转换成了 Object
,这样就会调用 remove(Object o)
去移除那个内容为 o
的元素。
为了高效地进行 Burrows–Wheeler 变换,我们需要环形后缀数组。
例如对于一个长度为 12 的字符串 ABRACADABRA!
,我们通过每次将其循环移动 1 位,可以得到 12 个不同的字符串(记为 Original Suffixes)。然后将这 12 个字符串按照字典序排序(记为 Sorted Suffixes)。
我们定义 index[i]
为排序后数组(Sorted Suffixes)中出现第 i
个原后缀(Original Suffixes)的索引。例如,index[11] = 2
意味着第 2
个原后缀(R A C A D A B R A ! A B
)出现在排序顺序中的第 11 位。
i Original Suffixes Sorted Suffixes 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
CircularSuffixArray 的任务是完成一个函数,方便获得 index[i]
。
注意由于空间复杂度要求是 n + R n+R n+R,所以我们不能求出整个 Sorted Suffixes 数组,因为长度为 n n n 的字符串一共可以有 n n n 个不同的循环表示,最终会得到一个 n 2 n^2 n2 的大小,但是又因为调用 index()
是需要在常数时间内完成的,所以我们必须在构造时就完成整个 index[]
数组的计算。
给定一个原后缀,它应该排在 Sorted Suffixes 数组中的第几位呢?
如果我们发现它的首字母是所有 n n n 个字符中第 k k k 大的,那它必定是在 Sorted Suffixes 数组中的第 k k k 或更大的位置上。例如,XABCYABDZ
中的 X
比 6 6 6 个字符大,所以它必定排在第 6 6 6 位甚至更大的位置上。
那如果是 BDZXABC