后缀数组

来源:互联网 发布:机顶盒有线网络连不上 编辑:程序博客网 时间:2024/05/18 02:57

后缀数组(suffix arrays)应该目前我看到的算法中最折磨人的一个了,(作者U.Manber:Suffix arrays: A new method for on-line string searches)
功能非常强大,字符串匹配的利器。举个小例子先。在多字符串匹配算法中,如果文本长度为n, 模式的数量为k,模式长度为m,KMP算法单字串匹配为线性复杂度,总的复杂度为O(nK)。如果采用后缀数组算法,先要对文本进行预处理,复杂度为O(nlogn),预处理后单个字串的匹配的复杂度是O(m+logn),总的复杂度为O(nlogn+K(m+logn))。当匹配模式的数量比较大,且文本长度比较长时,采用后缀数组效率能大幅提升。

下面先看看整体的思路,忽略一些细节。
后缀是指从某个位置i开始到整个字符串末尾的子串。Suffix(S,i)=S[i..len(S)]。
后缀数组保存了字符串所有后缀子串的字典序排列。举个小例子吧,字符串S=mississippi
把所有后缀数组进行排列:排第一的就是后缀子串Suffix(S,11)=i
1      i             suffix(S,11)
2     ippi           suffix(S,8)  
3     issippi        suffix(S,5)
4     ississippi     suffix(S,2)
5     mississippi     suffix(S,1)
6     pi              suffix(S,10)
7     ppi             suffix(S,9)
8     sippi            suffix(S,7)
9     sissippi         suffix(S,4)
10    ssippi           suffix(S,6)
11    ssissippi        suffix(S,3)

后缀数组Sa不必保存子字符串Suffix(S,i),只保存i的取值。在上面的例子中Sa[]={11,8,5,2,1,10,9,7,4,6,3}; Sa[1]=11的含义就是排第1的是Suffix(S,11)
如果所有后缀子串已经是排好序的,在字符串中寻找模式P=sip,就可以采用折半查找的方法,看是否存在后缀子串i,满足suffix(S,Sa[i])与模式P的最长公共前缀等于P的长度。查找过程的复杂度是O(m*logn),其中比较次数是logn的,每次比较字符串大小的代价与长度m是线性关系。

O(m*logn)比起KMP的O(n)感觉上提升得还不够,进一步改进效率,需要降低在每次字符串比较中O(m)复杂度,如果能提前判断模式P与查找位置的后缀子串前面的若干位是无需比较的就好了,这是求解最长公共前缀问题。
为了尽可能通过比较少的字符的比较就能判断出模式P与搜索区间的中间子串的大小,避免每次比较全部m个字符。U.Manber预先计算了后缀数组的任意两个子串的最长公共前缀长度记为LCP(Sa[i],Sa[j)), 由于后续处理都是在后缀数组上实现的,也可以简写为LCP(i,j).如在上面的例子中,issippi 与 ississippi  前四个字母相同,因此LCP(3,4)=4.

假设搜索的区间是(L,R)  M=(L+R)/2 , 区间两端与模式匹配的情况是已知的, 任意的LCP(i,j)已算好,
l=LCP(Suffix(S,Sa[L]),P)   r=LCP(Suffix(S,Sa[R]),P),折半查找就是要比较
先判断一下,l与r哪个更大,较大的能提供更多信息。 不妨设是l>=r吧,
这时能知道模式串P与Suffix(S,Sa[L])前l个是相同的, 去查询一下LCP(L,M),有两种可能
LCP(L,M)>=l, Suffix(S,Sa[L])与Suffix(S,Sa[M])前面有超过l个字母都是相同的,那么可以推断出Suffix(S,Sa[M])与模式P前l个字母肯定是相同的,只要从第l+1个字母开始进行逐一比较,找               到Suffix(S,Sa[M])与模式P的最长公共前缀,并确定折半搜索应该是在上半区还是下半区
LCP(L,M)<l ,  这时能推断出,Suffix(S,Sa[M])与模式P前LCP(L,M)个字母肯定是相同的,第LCP(L,M)+1个字母肯定是不同的,只需要比较一个字母,就能分出P与Suffix(S,Sa[M])的大小,确定搜索范围。

文字描述太绕,来个小例子吧,在字符串S=mississippi中搜索模式P=sip
初始L=1, R=11, l=LCP(i,sip)=0  r=LCP(ssissippi,sip)=1   M=(L+R)/2=6    计算l,r的比较次数是3,
r>l  查询LCP(M,R)=LCP(pi,ssissippi)=0,  LCP(M,R)<r, 只需要比较第一个字符就能判别sip与pi的大小,得到pi<sip,搜索范围在下半区,更新L=M=6, l=LCP(M,R)=0  r,R不变
M=(L+R)/2=8,  r>l   查询LCP(M,R)=LCP(sippi,ssissippi)=1, LCP(M,R)>=r, 前r个字符不需要比较了,从第r+1个字符开始逐一比较,得到LCP(sippi, sip)=3,等于模式P的长度,这说明找到一个结果。同时sip<sippi,
搜索范围在上半区, L,l不变, R=M=8, r=LCP(sippi, sip)=3,  更新M=7
r>l LCP(M,R)=0  LCP(M,R)<r,只需比较第LCP(M,R)+1个字符就能判别sip与ppi的大小, ppi<sip ,搜索范围在下半区, R,r不变, L=M=7, l=LCP(M,R)=0, 
此时 R-L==1 搜索结束。 共找到一个结果。

可以证明在所有logn 次比较中,比较的总字符数是O(m+logn)的。 至此,后缀数组算法的流程就大体清楚了,
先计算后缀数组,然后用折半查找的方法搜索模式串。但是,留下了两个没分析的问题:
1)怎么计算后缀数组Sa, 如果直接进行排序,复杂度应该是O(n^2logn)的,如何提高到O(nlogn)或者是线性的。
2)后缀数组的最长公共前缀LCP(i,j)是怎么计算的,如何在O(n)复杂度内搞定


http://blog.sina.com.cn/s/blog_69fd58a10100za77.html

原创粉丝点击