后缀数组可以使得在一个短文A中在线搜索一个字符串W的速度加快,用POS及lcp信息可以使得在线搜索时间达到O(P+logN)(P为W长度,N为A的长度)
下面简单介绍下何为后缀数组及其构造:
后缀:后缀是指从某个位置i开始到整个串末尾结束的一个特殊子串。字符串r的从第i个字符开始的后缀表示为Suffix(i),也就是Suffix(i)=r[i..len(r)]。(len(r)表示r的末尾结束)
大小比较:关于字符串的大小比较,是指通常所说的“字典顺序”比较,也就是对于两个字符串u、v,令i从1开始顺次比较u[i]和v[i]:
若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的两个开头位置不同的后缀 u和v进行比较的结果不可能是相等,因为 u=v的必要条件len(u)=len(v)在这里不可能满足。
后缀数组:后缀数组pos是一个一维数组,它保存1..n的某个排列pos[1],pos[2],……,pos[n],并且保证 Suffix(pos[i])<Suffix(pos[i+1]),1≤i<n。也就是将S的n个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入pos中。
名次数组:名次数组Rank[i]保存的是Suffix(i)在所有后缀中从小到大排列的“名次”。
简单的说,后缀数组是“排第几的是谁?”,名次数组是“你排第几?”。容易看出,后缀数组和名次数组为互逆运算,pos[i]=j,则Rank[j]=i。
以字符串babbaaab为例:
Rank = 6 3 7 5 0 1 2 4
b | a | b | b | a | a | a | b |
后缀数组排序(按字典序从小到大):
pos[0] = aaab (4)
pos[1] = aab (5)
pos[2] = ab (6)
pos[3] = abbaaab (1)
pos[4] = b (7)
pos[5] = baaab (3)
pos[6] = babbaaab (0)
pos[7] = bbaaab (2)
倍增算法的排序共进行O(logN)次,每次对长度为2^k的子字符串进行排序,求出排名,即Rank数组,k从0开始,每次加1,直到2^k>N,每个字符开始的长度为的子字符串即相当于所有的后缀。每次排序都利用上次长度为的字符串的排序值,则长度为的字符串可以用两个长度为的子字符串进行组合,然后进行基数排序,即可得到最终的pos值,每次排序时间复杂度为O(n),总的时间复杂度为O(nlog(n))。以字符串babbaaab为例:
b | a | b | b | a | a | a | b |
第一次排序k=0:(Rank数组如下) 长度为1
1 | 0 | 1 | 1 | 0 | 0 | 0 | 1 |
x y
1 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | - |
第二次排序k=1: (Rank数组如下) 长度为2
3 | 1 | 4 | 3 | 0 | 0 | 1 | 2 |
x y
2 | 3 | 1 | 2 | 3 | 0 | 2 | 0 | 0 | 1 | 0 | 2 | 1 | 0 | 2 | - |
第三次排序k=2: (Rank数组如下) 长度为4
6 | 3 | 7 | 5 | 0 | 1 | 2 | 4 |
Rank数组中无相同名次的字串,则该Rank数组即为最终排名,最终排列的后缀数组即为pos数组。
后缀数组的构建源代码
引入4个大小为n的临时数组及后缀数组Pos,空间复杂度为O(n),通过一个大的循环由单个到整体对所有的后缀进行排序,循环的时间复杂度为O(log(n)),基数排序的时间复杂度为O(n),所以总的时间复杂度为O(nlog(n)),为了实现简便及与实际需求相结合,该算法只识别字母,核心代码及解释如下:
int cmp(int* x,int i,int j,int k) {return x[i] == x[j] && x[i+k] == x[j+k];}
void sorting(char *s){
int n=strlen(s);
int m=0;//m记录不同关键字个数
int * x = (int*)malloc(sizeof(int) * n); //x数组用来记录后缀的k-前缀排名,可重复,实际上就是关键字。
int *wv = (int*)malloc(sizeof(int) * n); //对x数组中的名次计数
int * y = (int*)malloc(sizeof(int) * n); //y数组是辅助数组,用来保存按第二个关键字排序后的后缀的起始位置
int *ws = (int*)malloc(sizeof(int) * n); //记录按第二个关键字排序的后缀的第一个关键字的大小
/** 求关键字个数并构建x数组 **/
int alphabet[26] = {0};
for(int i = 0; i < n; i++) {//假设短文A全部由小写字母构成,为了简化运算,当然也可加入所有字符后进行计数,这应该不难,只是有点繁琐
else if(s[i]>='a'&&s[i]<='z') alphabet[s[i]-'a'] = 1;
}
for(int i = 0; i < 26; i++)
{
if(alphabet[i] == 1)
{
for(int k = 0; k < n; k++)
if(s[k]-'a' == i||s[k]-'A'==i)
x[k] = m;
m++;
}
}
/** 先对单独的一个字母进行排序 **/
for(int i=0;i<n;i++)ws[i]=0;
for(int i=0;i<n;i++)ws[x[i]]++;
for(int i=1;i<n;i++)ws[i]+=ws[i-1];//统计出不小于x[i]字符的所有字符
for(int i=n-1;i>=0;i--) Pos[--ws[x[i]]]=i;//Pos数组即为从小到大排列的元素
/** 求出最终的Pos数组,所有元素的两个关键字均不同时结束,采用基数排列**/
for(int j=1;m<n;j*=2){
/** 先对第二个关键字进行排列 **/
int t=0;
/**y[i]:从小到大第i名元素 x[i]:第i个元素是第几名**/
for(int i=n-j;i<n;i++)//第二关键字是0的排在最前面
y[t++]=i;
for(int i=0;i<n;i++)
if(Pos[i]>=j)y[t++]=Pos[i]-j;//字串Pos[i]是串Pos[i]-j的第二关键字,每次间隔j(小于j的元素无第二关键字)
/** 以第二关键字为基准对第一关键字进行排列 **/
for(int i=0;i<n;i++)wv[i]=x[y[i]];//提取每个字符串的第一关键字
for(int i=0;i<n;i++)ws[i]=0;
for(int i=0;i<n;i++)ws[wv[i]]++;
for(int i=1;i<n;i++)ws[i]+=ws[i-1];
for(int i=n-1;i>=0;i--)
Pos[--ws[wv[i]]]=y[i];
/** 重写x数组,使得具有相同关键字的字串具有相同排名 **/
int *p=x; x=y; y=p; //暂时用y数组贮存x数组内容
int f=1; //记录所有后缀都不同的关键字个数
x[Pos[0]]=0;
for(int i=1;i<n;i++){
x[Pos[i]]=cmp(y,Pos[i-1],Pos[i],j)?f:++f;//若长度为2*j的子串Pos[i]与Pos[i-1]完全相同,则他们有相同的排名
}
m=f;
}
}