后缀数组
前面有介绍过后缀树,后缀树对于我们针对某些字符串的处理使得如鱼得水,美中不足的是后缀树的代码实现复杂,只能让很多人望而却步。这次我们来介绍后缀树组:一个处理字符串的有力工具,也是一个后缀树的精美替代品,同样可以方便解决很多字符串问题。它比后缀树更加容易实现编码,也可以在不损失效率的情况下实现很多后缀树的功能,占用内存也比后缀树小很多,尤其是在模式匹配数据规模庞大的操作中实用性很高,很多搜索引擎也都在使用这样一个神奇的数据结构。
先来看看几个概念:
1> 假设有一个字符串:S 针对字符串S, Len(s) 表示字符串对应的长度
2>后缀Suffix(i): 对于字符串中的任意一个索引i开始到字符串结束的子串称之为该字符串S的一个后缀。比如suffix(i) = S[i...len(s)].
3>后缀数组SA(i):将一个字符串S的所有后缀串,按照字典顺序依次放入到一个数组中,这个数组表明了S的所有后缀串的字典顺序,这样一个有序的后缀串数组就是后缀数组。后缀数组SA(i)表示排名第i的字符串是谁?当然要注意的是这里的SA(i)的值是一个索引,是第i名的后缀串在原有串的起始索引值。
4>名次数组Rank(i): 名次数组是保存的是suffix(i)这个后缀在所有后缀串中的排名。不难发现名次数组和后缀数组是个互逆的排名。SA数组是表示排第几的是谁,而Rank则表示的是谁排第几。也就是说SA[Rank[i]] = i,从而也就是说在知道任意一个数组的情况下O(n)时间复杂度内快速求得另外一个数组。有了名次数组我们也可以在O(1)时间内求出任意两个后缀的大小关系。
如何构造后缀数组:
1> 将字符串S的所有后缀当成独立的字符串进行常规排序,平方级O(n^2)时间复杂度,因为忽略了所有后缀串之间的相互联系,将所有后缀当成独立的字符串进行排序从而构造效率低下,当然你对时间要求没那么高,数据规模没那么大当然可以轻松实现。这也不是本文的重点。
2> 根据罗穗骞的论文我们可以使用倍增算法(Doubling Algorithm),可以再O(nlogn)时间内构造出后缀数组,编码简单易行。
3> 罗穗骞的论文还提供了一种更为高效的构造算法:DC3算法,这是一个优秀的线性算法O(N)时间复杂度内构造后缀数组,编码相对于DA算法复杂。
本文重点描述倍增算法(DA)构造后缀数组,提供的倍增算法来自于罗穗骞的论文所描述的算法,这里仅提供对该算法的一些个人梳理,论文和所有求证过程需要自己去拜读原始论文《后缀数组——处理字符串的有力工具》。
倍增算法(DA):设字符串长度为n, 为了方便比较大小,可以再字符串的后面添加一个字符,这个字符没有在前面的字符中出现过,而且比前面的字符都要小,通常这个补充字符是0或者$。倍增算法就是用倍增的方法对每个字符开始的长度为2^k的子串进行排序,求出排名rank值。从k=0开始, 每次k加1, 当2^k大于n以后,每个子串开始的长度为2^k的子串便相当于所有的后缀。而且这些子串已经比较出大小了,所有的rank都没有相同的值,这个rank就是最后的结果。每一次排序都利用上一次的2^(k-1)的字符串的rank值, 那么长度为2^k的字符串的rank就可以用两个长度为2^(k-1)的字符串的排名为关键字表示出来,对关键字进行计数排序,便可以得到长度为2^k的字符串的rank。