后缀数组(基本概念及其构造方法之倍增算法)
来源:互联网 发布:海信网络电视多吗 编辑:程序博客网 时间:2024/05/22 02:21
本人可以说是菜鸟一枚,最近在学习字符串算法,学到后缀数组。从其中涉及的基本定义,到构造后缀数组方法中的其中一种倍增算法,光这些知识,就弄得我云里雾里,前前后后看了有三四天。看了很多大神的博客,提取并进行了一些总结,为加深记忆,在这里写下来,以便以后理解。若有理解不对之处,欢迎各位大神指正。
1、学习后缀数组,最先要明确的几个基本概念就是:子串、后缀、字符串的大小比较、后缀数组、名词数组这几个概念。(概念含义的描述基本都是大同小异了)
子串:很好理解,就是在原字符串中任意截取的一段字符串。加符号描述为:字符串S的子串为r[ i…j ],(i和j分别表示子串在原字符串中开始和结束的位置,其中i ≤ j),
即r[ i…j ]表示的就是i到j这一段S[i],S[i+1],...,S[j]顺次排列形成的字符串。
后缀:一个字符串的后缀,就是指从该字符串中的某个位置i开始,到整个串末尾结束的这样的子字符串。比如字符串”aabaaaab”,这个字符串的后缀有:”aabaaaab”, “abaaaab”, “baaaab”, “aaaab”, “aaab”, “aab”, “ab”, “b”. 通常,我们描述字符串S从i位置开头的后缀表示为Suffix(S,i),即Suffix(S,i)=r[i…len(S)]。
字符串的大小比较:就是将两个字符串从头开始依次按字典序比较。如对于字符串u和v,令i从1开始顺次比较u[i]和v[i](实际编码时从0位置开始), 如果u[i]=v[i],则i+1,否则若u[i]<v[i]则认为u<v,u[i]>v[i]则认为u>v(也就是v<u),比较结束。如果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,从位置i开头的后缀直接写成Suffix(i),省去Suffix(S,i)中的参数S。
后缀数组:后缀数组SA就是一个一维数组,其元素是SA[0],SA[1]……SA[n-1],保证suffix(SA[i]) < Suffix(SA[i+1]) , 0 ≤ i<n-1。意思就是,首先我们将字符串S的所有n个后缀从小到大排序,我们将排序后的每个后缀的开始位置依次放在SA[0],SA[1]……SA[n-1]中。
名次数组:名次数组Rank保存的是某一后缀在所有后缀中的排名(从小到大)。Rank[i]表示Suffix(i)在所有后缀中的排名。名次数组和后缀数组互为逆运算。有句话描述得很形象:后缀数组(SA)是 “ 排第几的是谁? ” ,名次数组(RANK)是 “你排第几? ”
2、基本概念了解完毕,下面开始了解构造后缀数组的方法。
构造后缀数组可采用倍增算法(Doubling Algorithm)或DC3算法(Difference Cover mod 3).其中,倍增算法相对于DC3算法更好理解,而DC3算法的效率则更高。
我目前只看了倍增算法。
倍增算法,利用各个后缀之间的有机联系,把最坏的时间复杂度降低为O(nlogn)。其核心思想属于递推。
整体思路可以说就是:由每个后缀的前K个字符的大小比较,从而得到前2k个字符的大小比较,依次类推……直至得到所有后缀的比较结果。我们把SAK称为k-后缀数组,其中元素SAK[0], SAK[1]……SAK[n-1]表示的是按照k-前缀(前k个字符)从小到大排序后的每个后缀的开始位置。Rankk含义参考SAK理解。若我们求出SAk和Rankk,我们就可以很方便地求出SA2k和Rank2k,为什么呢?
我们先来定义几个符号表示:uk, vk, <K, =K, ≤K
uk表示u的k-前缀(前k个字符), vk同左,
u <K v, 即表示uk< vk符号
u =K v, 即表示uk= vk符号
u ≤K v, 即表示uk≤vk符号
(这些式子就是在进行两个字符之间前k个字符的字典序比较)
根据我们定义的比较符的性质,我们可以得到如下性质:
性质1 对k≥n,Suffix(i) <KSuffix(j) 等价于 Suffix(i)<Suffix(j)。
性质2 Suffix(i) =2k Suffix(j)等价于Suffix(i) =kSuffix(j) 且 Suffix(i+k) =kSuffix(j+k)。
性质3 Suffix(i) <2k Suffix(j)等价于Suffix(i) <kSuffix(j) 或(Suffix(i) =kSuffix(j) 且Suffix(i+k) <kSuffix(j+k))。
我们再回过来看刚才的问题,就知道,根据性质2和3,我们可以看到2k-前缀的比较关系,可以由常数个k-前缀比较关系组合起来等价地表达。
于是,Suffix(i)和Suffix(j)在≤K关系下进行排序就相当于:每个Suffix(i)有一个主关键字Rankk[i]和一个次关键字Rankk[i+k]。所以,构造后缀数组的过程就是,先求出SA1和Rank1,接着在O(n)的时间(采用基数排序)求出SA2和Rank2,同样用O(n)的时间求出SA4和Rank4,这样依次类推,……,直到求出SAm和Rankm,此时m=2k,m>=n。又根据性质1,我们看到,此时的SAm和SA是等价的,这样,一共进行了logn次O(n)的过程,因此,在O(nlogn)的时间内计算出了后缀数组SA和名次数组Rank。
那到底如何来构造SA1和Rank1呢?很明显,求SA1和Rank1的过程就是对每个后缀的第一个字符进行比较排序。此时我们若采用快速排序,则时间复杂度是O(nlogn)。
接下来就是倍增算法的代码实现了。
我们来看看罗穗骞大神的代码实现。
intwa[maxn],wb[maxn],wv[maxn],ws[maxn];
int cmp(int *r,int a,int b,int l)
{return r[a]==r[b]&&r[a+l]==r[b+l];}
void da(int *r,int *sa,int n,int m)
{
int i,j,p,*x=wa,*y=wb,*t;
for(i=0;i<m;i++) ws[i]=0;
for(i=0;i<n;i++) ws[x[i]=r[i]]++;
for(i=1;i<m;i++) ws[i]+=ws[i-1];
for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i;
for(j=1,p=1;p<n;j*=2,m=p)
{
for(p=0,i=n-j;i<n;i++) y[p++]=i;
for(i=0;i<n;i++) if(sa[i]>=j)y[p++]=sa[i]-j;
for(i=0;i<n;i++) wv[i]=x[y[i]];
for(i=0;i<m;i++) ws[i]=0;
for(i=0;i<n;i++) ws[wv[i]]++;
for(i=1;i<m;i++) ws[i]+=ws[i-1];
for(i=n-1;i>=0;i--) sa[--ws[wv[i]]]=y[i];
for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
}
return;
}
现在我们来分段读一读代码的含义:
int wa[maxn],wb[maxn],wv[maxn],ws[maxn];定义了这样几个数组,具体是存放什么的我们在接下来用到时进行解释。
int cmp(int *r,int a,int b,int l)
{return r[a]==r[b]&&r[a+l]==r[b+l];}一个cmp比较函数,同样,为好理解,这个函数的作用在调用的时候我们再详细解释。
对于da函数中的这四行代码,实现的就是把字符串的每个后缀的第一个字符进行排序,最终得到SA1。其中r是字符串数组;这里的m表示的是,字符串中的最大值;n是字符串的长度,(这里包括我们在后面添加的特殊字符,因为它最小我们将它的值设为0),比如要构造后缀数组的字符串r是“aabaaaab”,则这里的n就是9; sa就是后缀数组了。
for(i=0;i<m;i++) ws[i]=0;
for(i=0;i<n;i++) ws[x[i]=r[i]]++;
for(i=1;i<m;i++) ws[i]+=ws[i-1];
for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i;
对于x[]和ws[]的含义,我们先从第一句和第二句的代码中来表面理解,x[i]=r[i]把r[i]赋给了x[i],并且ws[x[i]]++,当一二两个for循环执行结束后,此时的ws[]中是什么情况呢?就是字符串r中每个字符x[i]出现的次数存在了ws[x[i]]中。比如,我们给m赋值128,对于“aabaaaab”ws数组的情况就是ws[0]=1,ws[97]=6,ws[98]=2,其余元素均为0。接下来执行第三个for循环,这时ws[0]=1,ws[97]=7,ws[98]=9,接着执行第四个for循环,i从n-1开始到0,目的是对于同样的字符,使在前面出现的要排在前面。
总之,这四行代码很简洁地实现了对每个字符的排序,并放在了sa中,即求出了SA1。我只是了解了实现过程,至于大神是怎么想出来的我现在都不知道,待解救。(我说过了我是菜鸟……)。
继续看接下来的一个大的for循环里的内容:
在这个大的for循环中,我们用p的状态来作为循环结束条件。每次进行循环结束判断时的p就意味着每一步进行比较后的各个字符串不同Rank值的个数(实现方法在下面有解释),p的初始值我们设为1。也就是说,当p的值=n时,意味着字符串的所有后缀已经全部排序完毕,可以结束循环了。
for(j=1,p=1;p<n;j*=2,m=p)
{
这里的j,表示的是待合并的字符串的长度。
for(p=0,i=n-j;i<n;i++) y[p++]=i;
for(i=0;i<n;i++) if(sa[i]>=j)y[p++]=sa[i]-j;
上面这两句是对第二关键字进行排序,我们结合下面图的内容来理解。
( 对于这句:for(p=0,i=n-j;i<n;i++) y[p++]=i;
我们用y来存放按第二关键字排序后的字符串开始位置。
我们可以看到,每次字符串合并,位置n-j到位置n-1的这几个字符串都不在有字符串与其配对,其第二关键字必然为0,所以在进行第二关键字排序的时候,他们也都必然排在前面。
这一句:for(i=0;i<n;i++) if(sa[i]>=j)y[p++]=sa[i]-j;
由图可知:除去不能配对的字符串直接落下第二关键字是0,其余字符串的下面一行的第二关键字都是根据上面一行的排序结果得到,而且只有当开始位置sa[i]>=j的字符串才可以被拼接到sa[i]-j的位置上去形成新的长度为原来2倍的新的字符串。而且,很明显,按照sa[i]的顺序,rank[sa[i]]是递增的。所以这两个for循环就完成了按第二关键字对字符串的排序。)
for(i=0;i<n;i++) wv[i]=x[y[i]];
(wv[i]=x[y[i]];这条语句意思就相当于依次对按第二关键字排序后的每个字符串提取第一关键字,并存放到wv[ ]中。)
接下来就用下面的四个for循环来按第一关键字对合并后的字符串进行排序,最后更新sa道理同上面对字符串第一个字符进行排序的四个for循环。
for(i=0;i<m;i++) ws[i]=0;
for(i=0;i<n;i++) ws[wv[i]]++;
for(i=1;i<m;i++) ws[i]+=ws[i-1];
for(i=n-1;i>=0;i--) sa[--ws[wv[i]]]=y[i];
执行完以上代码后我们求出新的sa数组,现在要根据sa数组求出对应的Rank,即更新x[ ],我们的目的是要把新的Rank值放到x[ ]中,但是为了实现相同字符串的Rank值相同,我们必须要用到之前的Rank值,也就是之前的x[ ],而此时的y中存放的内容已经不再需要,所以在这里,我们把x和y都定义成指针的形式,运用交换指针的方式,可以轻松地实现用y来保存之前的rank值,最后实现x[ ]的更新。
代码实现如下。
for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
实质上,我们定义的x[ ]就是存放Rank1,Rank2,Rank4...的,而在程序最开始求sa1的过程中我们并没有真正在x[]中存放Rank值,而只是把r[i]赋给了x[i],可以实现x[ ]之间的大小比较而已(即相同的字符的Rank值相同)。
利用x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;语句,是将每次排序后各个字符串真正的rank值(即名次)P,最后放到x[ ]中。若cmp函数的返回结果是1(cmp函数的作用就是判断拼接后的两个字符串是否完全相同)则x[sa[i]]和x[sa[i-1]]的存放的p值相同。
}
这个大的for循环进行完第一次后,我们就可以知道,待排序字符串中Rank值的最大值也小于p,而我们发现ws数组中有意义的项即为ws[x[ ]]所以将p赋给m即可(m=p)。然后继续进行循环。
最后附上学习时的资料:
http://blog.csdn.net/jokes000/article/details/7839686
http://blog.csdn.net/wxfwxf328/article/details/7599929
http://www.doc88.com/p-7475947124562.html
http://tieba.baidu.com/f?kz=754580296
- 后缀数组(基本概念及其构造方法之倍增算法)
- 后缀数组 (由倍增算法构造)
- 后缀数组之倍增算法
- 后缀数组之倍增算法
- 倍增算法实现后缀数组的构造
- 2倍倍增算法构造后缀数组
- 后缀数组倍增构造算法说解
- 后缀数组 倍增算法
- 后缀数组,倍增算法
- 使用倍增算法(Prefix Doubling)构造后缀数组
- 后缀数组(SA倍增算法)
- 后缀数组--学习笔记(倍增算法)
- 后缀数组(倍增)
- 后缀数组(倍增)
- 倍增算法实现后缀数组
- 后缀数组倍增算法模版
- 后缀数组 倍增算法模板
- 后缀数组 倍增算法详解
- MySQL 存储过程那点事儿
- Linux进程间通信(IPC)编程实践(十二)Posix消息队列--基本API的使用
- HDU-3018-欧拉回路
- leetcode:Word Break
- 开启我的CSDN - 编程语言记录
- 后缀数组(基本概念及其构造方法之倍增算法)
- WebRTC 之二 浏览器的多媒体获取
- io流操作之读写示例代码(二)
- python编码风格pep8
- HTML5 input元素新增和改良的类型与其js验证
- io流操作之读写示例代码(一)
- 树中点对距离
- web前端学习路线(转)
- 2015-12-13复习之CSS3背景background的几个属性