后缀数组

Posted 陶无语

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了后缀数组相关的知识,希望对你有一定的参考价值。

  今天学习了一下后缀数组,感觉是一个较为复杂且精细的数据结构,要理解它最好只抓一些关键的部分。

  首先后缀数组是建立在一个字符串上的数据结构,其存储的元素是字符串的所有后缀,譬如‘abc‘的后缀有‘c‘,‘bc‘,‘abc‘,其起始下标分别为2,1,0。要存储所有的后缀显然需要花费O(n^2)级别的空间,对于超长的字符串这是不现实的。因此实现上往往用后缀的起始下标指代整个后缀,然后所有后缀可以共用源字符串,这样存储的压力就被解除了。

  显然直接使用后缀是比较困难的,一般需要对所有后缀进行排序,以方便之后的查找匹配。如果利用一般的字典来做,平衡树的时间复杂度为O(n^2log2(n)),而哈希表则为O(n^2)。无论是哪一种都无法应对文本足够大的情况。而后缀数组可以用于处理后缀的排序,其能够提供O(1)时间复杂度的起始下标与排名互转的快速操作,但是必须再之间建立完整的后缀数组。下面描述利用倍增算法构建后缀树的流程,其时间复杂度为O(nlog2(n)),空间复杂度为O(n),虽然不及隔壁的后缀树,但是胜在简单实用。

  我们先对所有后缀进行第一轮排序,排序的依据仅是后缀的第一个字符。由于字符集可能会较大(甚至会超过字符串的长度),因此可以使用快速排序完成这个过程。这里的时间复杂度为O(nlog2(n))。我们之后利用排序的先后顺序对每个后缀进行排名,如果两个后缀的第一个字符相同,则获得相同排名,排名是连续的,即为0,1,1,2之类不会出现断层,显然排名被约束到了0~n之间,即我们实际上用这次排序移除了字符集带来的干扰。但是我们现在还仅仅只是对第一个字符进行排序,难道还需要对后面每个字符进行一次排序吗?答案是否定的,我们可以用ranks来记录现在每个后缀的排名,ranks[i]表示以i为起始下标的后缀的排名。那么当我们要对前两个字符进行排序,我们发现此时排序的顺序与我们为每个后缀赋予关键字ranks[i]*n+ranks[i+1]并按照关键字对后缀进行排序后的顺序是一致的。当然对于最后一个后缀n-1,我们为其赋予关键字ranks[n-1]*n+0即可。这里的原因是字符串的比较规则,不理解可以自己体会一下。在我们第二轮排序都结束后,ranks赋值为新的排名,之后我们对前4个字符进行排序,显然排序后的结果和我们为每个后缀赋予关键字ranks[i]*n+ranks[i+2]后按关键字进行排序的结果是相同的。因此我们可以发现之后可以直接对前2^3,2^4,2^5...,2^k个字符先后进行排序,因此总共排序执行了O(log2(n))次,若我们采用快速排序或归并排序,时间复杂度为O(n(log2(n))^2)。等到所有排序完成,此时有2^k>n/2,而由于后面的第k+1次排序所有后缀被赋予关键字ranks[i]*n+0,这意味着所有排名都不会变动,因此我们只需要进行前k=ceil(log2(n/2)))次排序即可。而由于最后一次排序实际上相当与对所有后缀按前n个字符进行排名(不足补0,0视作比字符集中一切字符都小),而由于不同后缀的长度均不同,因此每个后缀都将拥有不同的排名,即排名和起始下标之间建立了双射关系。排序完后我们可以将ranks进行保存,同时将排序后的后缀序列进行保存,前者用于按起始下标查找对应排名,而后者用于按照排名查找起始下标,查找的时间复杂度显然为O(1)。

  由于我们每次排序后的ranks的数据范围为确定的0~n。因此我们可以使用另外一种排序技术,基数排序。我们以n为基数,这样只需要提供O(n)的空间就可以在O(n)时间复杂度内完成一趟排序。但是你可能要反问,我们排序使用的关键字是ranks[i]*n+ranks[i+2^k](其中2^k表示第k次排序)啊,其数据范围不是应该为0~n^2+n吗。是的,但是基数排序本身就是以基数r分配空间,并能在O(rlogr(n))时间复杂度内完成排序的算法,我们这里使用n为基数,而logn(n^2+n)=O(1),因此时间复杂度可以归结为O(n)。不懂的可以先百科一下基数排序。

  至此,我们可以利用基数排序替换上面的快速排序(除了第一次之外),这样时间复杂度就优化为O(nlog2(n)),空间复杂度为O(n)。

  

技术分享图片
  1 public class SuffixArray {
  2     int[] rank;
  3     int[] revRank;
  4     char[] data;
  5 
  6     private SuffixArray(int[] rank, int[] revRank, char[] data) {
  7         this.revRank = revRank;
  8         this.rank = rank;
  9         this.data = data;
 10     }
 11 
 12     public static SuffixArray makeSuffixArray(char[] s, int rangeFrom, int rangeTo) {
 13         int n = s.length;
 14         int range = n + 1;
 15         Loop<int[]> rankLoop = new Loop(new int[3][n + 1]);
 16 
 17         int[] orderedSuffix = new int[n + 1];
 18         int[] firstRanks = rankLoop.get(0);
 19         for (int i = 0; i < n; i++) {
 20             orderedSuffix[i] = i;
 21             firstRanks[i] = s[i] - rangeFrom + 1;
 22         }
 23         orderedSuffix[n] = n;
 24         firstRanks[n] = 0;
 25         Loop<int[]> suffixLoop = new Loop(new int[][]{
 26                 orderedSuffix, new int[n + 1]
 27         });
 28 
 29         radixSort(suffixLoop.get(0), suffixLoop.get(1), rankLoop.get(0), rangeTo - rangeFrom + 1);
 30         assignRank(suffixLoop.turn(), rankLoop.get(0), rankLoop.get(0), rankLoop.turn());
 31 
 32         for (int i = 1; i < n; i <<= 1) {
 33             System.arraycopy(rankLoop.get(0), i + 1, rankLoop.get(1), 1, range - i - 1);
 34             Arrays.fill(rankLoop.get(1), range - i + 1, range, 0);
 35             radixSort(suffixLoop.get(0), suffixLoop.turn(), rankLoop.get(1), range);
 36             radixSort(suffixLoop.get(0), suffixLoop.turn(), rankLoop.get(0), range);
 37             assignRank(suffixLoop.get(0), rankLoop.get(0), rankLoop.get(1), rankLoop.turn(2));
 38         }
 39 
 40         firstRanks = rankLoop.get(0);
 41         return new SuffixArray(firstRanks, suffixLoop.get(), s);
 42     }
 43 
 44     public static void assignRank(int[] seq, int[] firstKeys, int[] secondKeys, int[] rankOutput) {
 45         int cnt = 0;
 46         rankOutput[0] = 0;
 47         for (int i = 1, bound = seq.length; i < bound; i++) {
 48             if (firstKeys[seq[i - 1]] != firstKeys[seq[i]] ||
 49                     secondKeys[seq[i - 1]] != secondKeys[seq[i]]) {
 50                 cnt++;
 51             }
 52             rankOutput[seq[i]] = cnt;
 53         }
 54     }
 55 
 56     public static void radixSort(int[] oldSeq, int[] newSeq, int[] seqRanks, int range) {
 57         int[] counters = new int[range];
 58         for (int rank : seqRanks) {
 59             counters[rank]++;
 60         }
 61         int[] ranks = new int[range];
 62         ranks[0] = 0;
 63         for (int i = 1; i < range; i++) {
 64             ranks[i] = ranks[i - 1] + (counters[i] > 0 ? 1 : 0);
 65             counters[i] += counters[i - 1];
 66         }
 67 
 68         for (int i = oldSeq.length - 1; i >= 0; i--) {
 69             int newPos = --counters[seqRanks[oldSeq[i]]];
 70             newSeq[newPos] = oldSeq[i];
 71         }
 72     }
 73 
 74     public int getStartIndexByRank(int rank) {
 75         return revRank[rank];
 76     }
 77 
 78     public int getRankByStartIndex(int startIndex) {
 79         return rank[startIndex];
 80     }
 81 
 82     public static class Loop<T> {
 83         T[] loops;
 84         int offset;
 85 
 86         public Loop(T[] initVal) {
 87             loops = initVal;
 88         }
 89 
 90         public T get(int index) {
 91             return loops[(offset + index) % loops.length];
 92         }
 93 
 94         public T get() {
 95             return get(0);
 96         }
 97 
 98         public T turn(int degree) {
 99             offset += degree;
100             return get(0);
101         }
102 
103         public T turn() {
104             return turn(1);
105         }
106     }
107     
108     @Override 
109     public String toString() {
110         StringBuilder result = new StringBuilder();
111         for (int i = 1, bound = revRank.length; i < bound; i++) {
112             result.append(i).append(" : ").append(new String(data, revRank[i], data.length - revRank[i])).append(";\n ");
113         }
114         return result.toString();
115     }
116 }
View Code

 

  同时再介绍一种利用后缀数组以O(n+m)时间复杂度内计算两个字符串最大匹配子串的方式。首先我们用一个特殊的字符(不存在两个字符串之中)作为中间字符连接两个字符串为新的字符串S,记连接字符的位置为p。之后在合并后的字符串上建立后缀数组。我们记heights[i]表示排名为i的后缀与排名为i-1的后缀之间的最长相同前缀长度,那么我们的问题就变成了最大的heights[i],满足排名为i和i-1的后缀的起始下标一者大于p,一者小于p(由于S[p]是唯一字符,因此不会S[p]不会和任意其它字符相同,即匹配不会越过p)。heights用一般的方法建立需要付出O(n^2)的时间复杂度才行,但是我们可以利用一个简单的思路,设排名为i的后缀起始下标为i‘,记j‘=i‘-1,而j为j‘后缀的排名,显然heights[i]>=heights[j]-1,因此我们按照后缀的起始下标顺序分别对每个后缀计算其对应的height值,就可以在O(n)的时间复杂度内计算完整个heights数组。这里利用了一个命题,设排名i,j,k递增, 则排名为i的后缀与排名为k的后缀的最长匹配前缀必然不可能长于排名为j与k的后缀的最长匹配前缀的长度。

以上是关于后缀数组的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段—— 数组的响应式方法

VSCode自定义代码片段10—— 数组的响应式方法

Sublime Text3自定义代码片段

后缀数组代码详解

●后缀数组○十三个例题

初学后缀数组记录(然而并不是很会。。&&很水。。)