倍增算法实现后缀数组的构造

后缀数组可以使得在一个短文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为例:

第一次排序k=0:(Rank数组如下)                      长度为1

                  x  y

1

0

0

1

1

1

1

0

0

0

0

0

0

1

1

-

第二次排序k=1: (Rank数组如下)                      长度为2

                  x  y

2

3

1

2

3

0

2

0

0

1

0

2

1

0

2

-

第三次排序k=2: (Rank数组如下)                      长度为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;

}


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值