把一个字符串中的所有后缀以其开始位置编号,按字典顺序排列这些后缀后,把相应编号存放在一个数组中,这个数组就是这个字符串的后缀数组。
以字符串 aboreabo为例,把它的所有后缀按字典顺序排列后为:
abo
aboreabo
bo
boreabo
eabo
o
oreabo
reabo
这个字符串的后缀数组为{5,0,6,1,4,7,2,3}。
Manber和Myers在《Suffix arrays: A new method for on-line string searches》中提出后缀数组,
并给出一种算法复杂度为O(nlogn)的算法,04年国家集训队有篇关于后缀数组的论文,就是用的这个算法。
2006年,Karkkainen等人在《Linear Work Suffix Array Construction》中提出了DC3(difference cover)算法,可以在用O(n)的时间复杂度构建出后缀数组,并且在文章末尾给出了使用C++的实现代码。
后缀数组是个很好用的数据结构,在求单个字符串的重复子串和多个字符串的公共子串时,应该优先考虑是否可以使用后缀数组。
下面的程序就是利用后缀数组求一个字符串的最长公共子串的算法。这个算法主要利用各个后缀之间的关系,因为对于同一个字符串的任意两个后缀,较短后缀也是较长后缀的后缀。
每轮排序都以相应后缀的定长前缀为关键字来排序,前缀长度的选取为2的倍数,分别为1,2,4,… 每轮排序后所得后缀数组依次记做S1,S2,S3,…
首先,以每个后缀的首字符为关键字,进行桶式排序,复杂度为O(n);
假如在当前阶段,关键字的长度为h,其中h为2的倍数,所有前h个关键字相同的后缀都存放在同一个h桶中;那么下个阶段排序使用的关键字长度为2h,排序时就可以使用前一阶段的结果,排序后前2h个关键字相同的后缀将存放在同一个2h桶中。
B1 B2 B3 B4 B5
——————— —— ——————— ——————— ———
atta...
athl…
envy…
erro…
er…
hl…
hl…
ta…
↑Ai ↑Aj ↑Aj+h ↑Ai+h
上图所示是以前2个字符为关键字的第二轮排序完成后所得的后缀数组S2,图中后缀共存放在5个桶中,分别为B1,…,B5。
在第三轮排序时,使用前4个字符为关键字,上轮排序后位于不同的桶中的后缀相对顺序已经确定,所以只需把上轮位于相同的桶中的后缀按前4个关键字划分为不同的桶即可。在本论中比较后缀Ai和Aj,由于Ai和Aj前2个关键字相同,Ai+h和Aj+h分别是Ai和Aj去掉前2个字符后的后缀,所以只需比较Ai+h和Aj+h即可,而在第二轮已经得到 Ai+h和Aj+h的相对顺序,因Ai+h > Aj+h,所以得Ai > Aj。
B B2 B3 B4 B5 B6
——— ——— ——— ——————— ——————— ———
athl...
atta…
envy…
erro…
er…
hl…
hl…
ta…
↑Ai<---->↑Aj ↑Aj+h ↑Ai+h
从头到尾遍历上一轮的后缀数组,对于每个Ai, 把Ai-h前移到所在桶的下一个填充位置,则得到本轮的后缀数组,然后可划分新的桶。
当所有的元素都位于不同的桶时,排序完成,得到所需的后缀数组。
最多需排序O(logn)轮,每轮所需时间为O(n),所以算法总的复杂度为O(nlogn)。
下面是通过构造后缀数组来求给定字符串最长重复子串的程序。其中的两个子函数主要是JGShining实现的,但是我调试的时候发现存在的一些问题,对这两个函数加以改进和完善,主要修改了以下部分:
比如Rank数组越界,以及当字符串由同一字符重复组成时不能得出正确的height数组。
一、原方法在求S(2h)时采用逆向遍历后缀数组,由高端到低端的顺序填充桶;我使用正向遍历的方法,由低端到高端的顺序填充桶;
二、修改了Rank(2h)的计算方法,原方法在一些情况下不能得到正确的Rank数组;
三、修改了Height数组的计算方法,原方法在字符串由同一字符重复组成的情况下不能得到正确的height数组;
四、简化了两个子函数的接口。
JGShining的代码可以在http://jgshining.cn/blog/post/suffix_array_implemention.php找到。