• 后缀数组


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

      首先后缀数组是建立在一个字符串上的数据结构,其存储的元素是字符串的所有后缀,譬如'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(";
     ");
    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的后缀的最长匹配前缀的长度。

  • 相关阅读:
    职业生涯系列
    自我进修系列
    每周问题系列
    职业生涯系列
    软件测试专用名词
    Java系列 – 用Java8新特性进行Java开发太爽了(续)
    Java系列
    EJB系列
    EJB系列
    EJB系列
  • 原文地址:https://www.cnblogs.com/dalt/p/8035454.html
Copyright © 2020-2023  润新知