算法思路

主要是基于基数排序,如果基数排序没弄懂代码就会很难理解:

  1. 首先从k=0开始,从后缀数组里面选取步长为2^k的后缀数组的前子串
  2. 然后进行基数排序
  3. 如果排序后所有的名次数组的值都不相同,那么排序结束;
  4. 否则,k++(也就是步长翻倍),继续排序。

几个概念

数组sa(sorted array):构造完成前表示关键字数组,下标表示名次,值表示关键字的首字符位置,值相同的时候名次根据在原串中相对位置的先后决定;构造完成后表示后缀数组,下标表示名次,值表示后缀的首字符位置。 比如'abb'的sa: sa[0] = 0 sa[1] = 2 sa[2] = 1

数组x:表示rank数组,下标表示关键字的位置,值表示关键字大小(rank),相同的值有相同的rank。初始化为字符串r的每个字符大小(此时x并不代表rank,只借助其值比较相对大小)。在每次迭代后,根据sa重新赋值,并代表rank。

数组y:排序后的第二关键字数组,下标表示名次,值代表第二关键字的首字符位置。也就是类似于第二关键字的sa数组。

注意的是右图中的y不是代码中的y数组,右图的y只是演示表示第二关键字的排名。

    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++)  //分为128个桶来进行基数排序ws[i] += ws[i-1];for(i = n-1; i >= 0; i--) sa[--ws[x[i]]] = i;复制代码

上面代码的意思是,首先我们对数组初始化,m初始的时候我们简单采用ASICII码的m=128。要对长度为1的字符串进行排序,这样就求出了一个sa。

        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++; }
复制代码

这段代码非常精妙,也很难理解。 接下来我们从步长为1开始逐步倍增步长来进行基数排序,基数排序要分两次,第一次是对第二关键字排序,第二次是对第一关键字排序。对第二关键字排序的结果实际上可以利用上一次求得的sa直接算出,没有必要再算一次。那么怎么算呢?

  • 首先是 for(p=0,i=n-j;i<n;i++) y[p++]=i; 这段的意思是下标i加上步长j如果超过了n,那么很明显第二关键字已经不存在了,那么越短的肯定要排在最前,同时我们按照原来的顺序排序保证算法的稳定。以长度为4时为例,下标超过4(图中rank为2)时的关键字将不存在第二关键字。

  • for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j; 这段的意思是,其余部分可以利用第一关键字的排序进行。但是第二关键字的下标和第一关键字的下标是不一样的,第一关键字的值对应下标就是该关键字的下标,第二关键字的值的下标减去j才是对应关键字的下标。举个例子,上面的第一个组合关键字是(4,2)下标为0。其中4是第一关键字下标0;2是第二关键字下标是4,需要减去长度4才能对应到组合关键字的下标。

  • wv[i] = x[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++;

这一段也不好理解,目的呢是生成rank数组x。但是如果我们按照sa的方式简单映射,x[i]肯定是彼此不相同的。但是实际上当前步长下有可能他们的rank是相同的(图中的例子是不存在的,因为才排了一次rank就彼此不相同了)。 这里由于y数组实际上已经没用了,为了节省空间,我们交换x,y用y来保存rank值。

那么怎么判定重复? int cmp(int *r , int a, int b, int l) { return r[a] == r[b] && r[a+l] == r[b+l]; } 这段也需要仔细探讨下,我们直到最直接的办法是相邻的两个后缀数组从后缀数组的头一直比较到尾部,但是这里只比较了第一关键字和第二关键字对应的字符就可以了,这是为什么?因为我们直到假设当前步长是2^k,第一,二个关键字分别相当于前后2^(k-1)个字符的名次,名次如果都相同那整个字符串肯定都相同了.

并且,如果r[a]=r[b],说明以r[a]或r[b]开头的长度为l的字符串肯定不包括字符r[n-1](否则,那么他们的长度肯定不同,r[a],r[b]这两个名次必然不一样),所以调用变量r[a+l]和r[b+l]不会导致数组下标越界,这样就不需要做特殊判断。(这点也真的很妙)

还有两个细节,for(j=1,p=1;p<n;j=2,m=p) {…………}

  • 在第一次排序以后,rank数组中的最大值小于p,所以让m=p。
  • 变量p的结果实际上就是不同的字符串的个数。这里可以加一个小优化,如果p等于n,那么函数可以结束。因为在当前长度的字符串中,已经没有相同的字符串,接下来的排序不会改变rank值。例如图2中的第四次排序,实际上是没有必要的。

算法分析

每次基数排序是O(n),排序的次数决定于最长公共子串的长度,最坏情况需要循环logn次,所以总的复杂度为O(nlogn)。

空间上,引入了第二关键字排序数组y,rank数组x,计数器ws,中间数组wv。加上原本需要的r与sa以及对字串串数组转换成的int数组,一共是7n,空间要求为 O(n).

完整代码

#include<stdio.h>
#include<cstring>
#define maxn 100
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];
}
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 <<= 1,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]]; //x相当于rank数组,最大值肯定是小于p的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++;  //可以看出x[i]一定小于p}
}//测试代码
int main(){const char r_str[] = "aadacabaababab";int len = strlen(r_str);int r[len];int sa[len];for(int i=0;i<len;r[i]=r_str[i],i++);for(int i=0;i<len;i++){printf("%d\n",r[i]);}da(r,sa,len,128);  for(int i=0;i<len;i++){printf("%d\t%d\t",i,sa[i]);for(int j=sa[i];j<len;j++)printf("%c",r_str[j]);printf("\n");}
}
复制代码

后缀数组 倍增法详解相关推荐

  1. 单链表的头尾插法详解

    单链表头尾插法详解 头插法构造单链表 代码实现 头插法过程 尾插法构造单链表 代码实现 尾插法过程 单链表头尾插法对比 #include "stdio.h" #include &q ...

  2. ES5和ES6数组遍历方法详解

    ES5和ES6数组遍历方法详解 在ES5中常用的10种数组遍历方法: 1.原始的for循环语句 2.Array.prototype.forEach数组对象内置方法 3.Array.prototype. ...

  3. 【bzoj3879】SvT 后缀数组+倍增RMQ+单调栈

    题目描述 (我并不想告诉你题目名字是什么鬼) 有一个长度为n的仅包含小写字母的字符串S,下标范围为[1,n]. 现在有若干组询问,对于每一个询问,我们给出若干个后缀(以其在S中出现的起始位置来表示), ...

  4. 旋转排序数组系列题详解

    旋转排序数组系列题详解 文章目录 旋转排序数组系列题详解 一.问题描述:旋转数组的最小数字 二.分析:二分查找 三.代码 四.问题描述:寻找旋转排序数组中的最小值 五.分析:二分搜索 六.代码 七.问 ...

  5. 安卓miracast花屏_创维酷开电视多屏互动Miracast玩法详解

    创维酷开电视多屏互动Miracast玩法详解 安卓手机是可以通过多屏互动Miracast玩法直接让我们手机与创维酷开电视进行无线投屏的,但是有些创维电视的Miracast功能找不到怎么办? 创维酷开电 ...

  6. 视频教程-沐风老师3DMAX室内建模挤出法详解-3Dmax

    沐风老师3DMAX室内建模挤出法详解 沐风课堂创始人,专栏作家,独立媒体人,资深互联网从业者. 沐风老师 ¥12.00 立即订阅 扫码下载「CSDN程序员学院APP」,1000+技术好课免费看 APP ...

  7. java指数表示法_Java指数计数法详解

    Java指数计数法详解 时间:2017-10-16     来源:华清远见Java培训中心 Java指数计数法并不是一个很难的运算,关键是你要理解应用,很多朋友不理解Java指数计数法,所以也无从运用 ...

  8. 创维linux怎么连接wifi,创维酷开电视多屏互动Miracast玩法详解

    创维酷开电视多屏互动Miracast玩法详解 安卓手机是可以通过多屏互动Miracast玩法直接让我们手机与创维酷开电视进行无线投屏的,但是有些创维电视的Miracast功能找不到怎么办? 创维酷开电 ...

  9. 一文速学数模-时序预测模型(四)二次指数平滑法和三次指数平滑法详解+Python代码实现

    目录 前言 二次指数平滑法(Holt's linear trend method) 1.定义 2.公式 二次指数平滑值: 二次指数平滑数学模型: 3.案例实现 三次指数平滑法(Holt-Winters ...

最新文章

  1. 推荐算法-聚类-DBSCAN
  2. centos文本查看及处理相关的常用命令
  3. 环境监控告警系统之TIM即时消息推送部署(二)
  4. 嵌入式linux图形系统设计,轻量级嵌入式Linux图形系统设计与实现
  5. 洛谷P1534题解(Java语言描述)
  6. 14日晚8点直播丨 经典知识库:性能优化那些事
  7. Golang并发模式--channel高级使用
  8. WCF中的REST是什么
  9. 此计算机屏保怎么取消,如何取消屏幕保护
  10. Oracle如何导出存储过程
  11. 详解LDC架构-设计业务异地多活架构
  12. php夜间时间模式,Typecho夜间模式设置
  13. 汇编语言实验二 汇编语言程序设计(顺序、多分支、循环)
  14. 微信小程序苹果手机statusBarHeight状态栏高度为0
  15. 录屏——制作gif图片——压缩图片大小
  16. 【Win8自带微软输入法删除图解】
  17. POJ 1061 青蛙的约会
  18. 如何在cad中导入谷歌地图_如何在Google地图中避开收费公路
  19. 使用a标签时不用href=““调转页面
  20. maya 白天室内灯光_求解maya室内灯光怎么打

热门文章

  1. log4j教程_Log4j教程
  2. wordpress编辑插件_如何使用Tabify编辑屏幕插件减少WordPress帖子编辑器屏幕的拥挤
  3. NavigationView内的Android ExpandableListView
  4. 理解类路径是什么意思?如何运用包?
  5. springboot新版本(2.1.0)、springcloud新版本(Greenwich.M1)实现链路追踪的一些坑
  6. 移动端手势库设计与实践
  7. 55.SQL server 行转列
  8. 受够了碎片化,Salesforce决定只支持部分安卓设备
  9. 《Power Designer系统分析与建模实战》——1.4 本章小结
  10. 关于自定义异常中为什么带参构造器需要显示调用父类异常的带参构造器