字符串——后缀数组

字符串——后缀数组

学习资料:论文链接

名词概念

后缀 Suffix[i]表示从i到字符串末尾的一个子串,

字符串大小比较 指按字典顺序比较,对于两个字符串u、v,令i=1,比较u[i]v[i] ,若相等则i+=1,若u[i] < v[i],则 u<v,反之亦然。如果 i > len(u) 或者 i > len(v) 仍比较不出结果,那么,若 len(u) < len(v) 则 u < v ,若 len(u) = len(v)则 u = v ,若 len(u) > len(v)则 u > v。由此可知字符串的任意两个后缀,因其长度不等,大小亦不相等。

后缀数组 一个字符串s,其长度为n,它的后缀数组SA是一个一维数组,该数组保存的是[1…n]的某个排列,该排列是后缀按大小升序排列后的i值,满足Suffix(SA[j]) < Suffix[SA[j+1]]

例:s = ‘ aabbaca ‘

名次数组 Rank[i] 保存的是后缀按从小到大顺序排列后,Suffix[i]的名次。例:Rank[1] = 2,Rank[4] = 3

问题描述

给定文本T,其长度为n;查询样本为P,其长度为m

如何利用后缀数组在T中快速查找P的位置?

利用性质:一个字符串的任何子串都可以看做是它某个后缀的一个前缀。

我们可以利用二分查找寻找到子串的位置。算法如下:

​ 初始化 j = n/2

​ 比较 T[ SA[j] : n-1 ] 和 P,此次比较需要至多 m次比较,若 T[ SA[j] : n-1 ] = P 则,返回SA[j];若 P < T[ SA[j] : n-1 ],则查找SA[j]之前的后缀;若 P > T[ SA[j] : n-1 ],则查找SA[j]之后的后缀。

例:在s = ‘ aabbaca ‘ 中查找 ‘ ab ‘

binary_search

由ab是abbaca的前缀可知ab的位置为 SA[2] = 1。

其时间复杂度为O(mlogn)。

如何找出T中所有的P?

多个P的存在,说明在S的所有后缀中,有多个后缀具有相同的前缀。因为SA是按照后缀的字典顺序排序的,因此这些具有相同前缀P的后缀在SA中的位置是连续的。根据此性质,我们可以用两个二分查找去锁定SA中的这段连续范围。其时间复杂度为O(mlogn+k),k为P的数量。

例:在s = ‘ aabbaca ‘ 中查找子串 ‘ a ‘ 的出现次数

首先利用二分查找到做左边界,得 j=0,SA[j] = 6

同样使用二分查找,找到右边界,得 j=3,SA[3] = 4

seach_all_a_right

SA[0] 到 SA[3] 共4个后缀的前缀为 ‘a’ ,得共出现4次。

如何构建后缀数组

倍增算法

利用新建rank数组用来存储后缀的排序。

construct_sa

图片来自论文-论文链接

这张图其实很好的解释了最终rank是如何产生的。我们会发现后缀都是由一个较短的已知后缀和前半部分组成的。从图中我们可以看到,从第三次排序到第四次排序之间的组合,就已经把所有后缀的包括在内了,这里的排序采用基数排序,从之前获取的排序信息,可以很容易得到组合后的排序。

例,求 ‘abcaabccabcac’ 的后缀数组:

xampl

SA = [3, 0, 8, 4, 11, 1, 9, 5, 12, 2, 7,10, 6]