字符串匹配(三)----后缀数组算法

一、什么是后缀数组:

  字符串后缀Suffix 指的是从字符串的某个位置开始到其末尾的字符串子串。后缀数组 Suffix Array(sa) 指的是将某个字符串的所有后缀按字典序排序之后得到的数组,不过数组中不直接保存所有的后缀子串,只要记录后缀的起始下标就好了。

  比如下面在下面这张图中,sa[8] = 7,表示在字典序中排第9的是起始下标为7的后缀子串,这里还有一个比较重要的数组rank,rank[i] : sa[i]在所有后缀中的排名 ,比如rk[5]=0,表示后缀下标为5的子串在后缀数组中排第0个; rank数组与sa数组互为逆运算,rk[sa[i]]=i;

    

  现在假如我们已经求出来了后缀数组,然后直接对已经排好序的后缀数组进行二分查找,这样就能匹配成功了,下面贴出代码:

import java.util.Arrays;

public class SuffixArrayTest {
    public static void main(String[] args) {
        match();  // 得到结果是5
    }

    static void match(){
        String s = "ABABABABB";
        String p = "BABB";
        Suff[] sa = getSa(s); // 后缀数组
        int l = 0;
        int r = s.length()-1;
        // 二分查找 ,nlog(m)
        while(r>=l){
            int mid = l + ((r-l)>>1);
            // 居中的后缀
            Suff midSuff = sa[mid];
            String suffStr = midSuff.str;
            int compareRes;
            // 将后缀和模式串比较,O(n);
            if (suffStr.length()>=p.length()) {
                compareRes = suffStr.substring(0, p.length()).compareTo(p);
            }else {
                compareRes = suffStr.compareTo(p);
            }
            // 相等了 输出后缀的起始位置
            if(compareRes == 0){
                System.out.println(midSuff.index);
                break;
            }else if (compareRes<0) {
                l = mid + 1;
            }else {
                r = mid - 1;
            }
        }
    }
    /**
     * 直接对所有后缀排序,因为字符串的比较消耗O(N),所以整体为N²log(N)
     * @param src
     * @return
     */
    public static Suff[] getSa(String src){
        int strLength = src.length();
        // sa 即SuffixArray,后缀数组
        // sa 是排名到下标的映射,即sa[i]=k说明排名为i的后缀是从k开始的
        Suff[] suffixArray = new Suff[strLength];
        for (int i = 0; i < strLength; i++) {
            String suffI = src.substring(i);   //截取后缀
            suffixArray[i] = new Suff(suffI, i);
        }
        Arrays.sort(suffixArray);   //依据Suff的比较规则进行排序
        return suffixArray;
    }

    static class Suff implements Comparable<Suff>{

        String str;  //后缀内容
        int index;   //后缀的起始下标

        public Suff(String str, int index) {
            super();
            this.str = str;
            this.index = index;
        }

        @Override
        public int compareTo(Suff o2) {
            return this.str.compareTo(o2.str);
        }
        @Override
        public String toString() {
            return "Suff{"+"str=‘"+str+"\‘"+",index="+index+"}";
        }

    }
}

二、倍增法  

  上面求后缀数组的方式时间复杂度为n²log(n),一般来说,时间复杂度只要达到了n平方级别都要想办法降低,于是就有一种叫做倍增法的方法来求后缀数组,基本思想就是:

   1、先将每个字符排序 得到sa,rank数组,

   2、然后给每个字符增添一个字符,这样就变成了两个字符,最后一个字符无法增添字符,就需要处理好边界问题。然后就是排序,排序规则的话就需要自定义规则

   3、然后再在两个字符的基础上添加两个字符,就变成四个字符,然后再在上一次排序的规则上进一步排序。然后八个字符......

  最主要的降低时间复杂度的方式就是根据每一步更新后的rank数组来进行下一步的排序,这样前面已经排好序的就不用比较了。嗯。。。具体的倍增法的思想的话只有自己在具体应用代码的时候慢慢琢磨,通过不断地调试慢慢理解。这里的代码直接都是封装好了方法直接调用即可。下面贴出代码:

  1 import java.util.Arrays;
  2
  3 public class SuffixArray {
  4     public static void main(String[] args) {
  5         match();  // 得到结果是5
  6     }
  7
  8     static void match(){
  9         String s = "ABABABABB";
 10         String p = "BABB";
 11 //        SuffixArray.Suff[] sa = SuffixArray.getSa(s); // 后缀数组
 12         Suff[] sa = getSa2(s); // 后缀数组
 13         int l = 0;
 14         int r = s.length()-1;
 15         // 二分查找 ,nlog(m)
 16         while(r>=l){
 17             int mid = l + ((r-l)>>1);
 18             // 居中的后缀
 19             Suff midSuff = sa[mid];
 20 //            String suffStr = midSuff.str;
 21             String suffStr = s.substring(midSuff.index);
 22             int compareRes;
 23             // 将后缀和模式串比较,O(n);
 24             if (suffStr.length()>=p.length()) {
 25                 compareRes = suffStr.substring(0, p.length()).compareTo(p);
 26             }else {
 27                 compareRes = suffStr.compareTo(p);
 28             }
 29             // 相等了 输出后缀的起始位置
 30             if(compareRes == 0){
 31                 System.out.println(midSuff.index);
 32                 break;
 33             }else if (compareRes<0) {
 34                 l = mid + 1;
 35             }else {
 36                 r = mid - 1;
 37             }
 38         }
 39     }
 40
 41
 42     /**
 43      * nlg²n 构建后缀数组
 44      *
 45      * @param src
 46      * @return
 47      */
 48     public static Suff[] getSa2(String src) {
 49         int n = src.length();
 50         Suff[] sa = new Suff[n];
 51         for (int i = 0; i < n; i++) {
 52             sa[i] = new Suff(src.charAt(i), i, src);// 存单个字符,接下来排序
 53         }
 54         Arrays.sort(sa);
 55
 56         /** rk是下标到排名的映射 */
 57         int[] rk = new int[n];// suffix array
 58         rk[sa[0].index] = 1;
 59         for (int i = 1; i < n; i++) {
 60             rk[sa[i].index] = rk[sa[i - 1].index];
 61             if (sa[i].c != sa[i - 1].c)
 62                 rk[sa[i].index]++;
 63         }
 64         // 倍增法
 65         for (int k = 2; rk[sa[n - 1].index] < n; k *= 2) {
 66
 67             final int kk = k;
 68             Arrays.sort(sa, (o1, o2) -> {
 69                 // 不是基于字符串比较,而是利用之前的rank
 70                 int i = o1.index;
 71                 int j = o2.index;
 72                 if (rk[i] == rk[j]) {// 如果第一关键字相同
 73                     if (i + kk / 2 >= n || j + kk / 2 >= n)
 74                         return -(i - j);// 如果某个后缀不具有第二关键字,那肯定较小,索引靠后的更小
 75                     return rk[i + kk / 2] - rk[j + kk / 2];
 76
 77                 } else {
 78                     return rk[i] - rk[j];
 79                 }
 80             });
 81             /*---排序 end---*/
 82             // 更新rank
 83             rk[sa[0].index] = 1;
 84             for (int i = 1; i < n; i++) {
 85                 int i1 = sa[i].index;
 86                 int i2 = sa[i - 1].index;
 87                 rk[i1] = rk[i2];
 88                 try {
 89                     if (!src.substring(i1, i1 + kk).equals(src.substring(i2, i2 + kk)))
 90                         rk[i1]++;
 91                 } catch (Exception e) {
 92                     rk[i1]++;
 93                 }
 94             }
 95         }
 96
 97         return sa;
 98     }
 99
100     public static class Suff implements Comparable<Suff> {
101         public char c;// 后缀内容
102         private String src;
103         public int index;// 后缀的起始下标
104
105         public Suff(char c, int index, String src) {
106             this.c = c;
107             this.index = index;
108             this.src = src;
109         }
110
111         @Override
112         public int compareTo(Suff o2) {
113             return this.c - o2.c;
114         }
115
116         @Override
117         public String toString() {
118             return "Suff{" + "char=‘" + src.substring(index) + ‘\‘‘ + ", index=" + index + ‘}‘;
119         }
120     }
121 }

三、高度数组

  高度数组是后缀数组伴生的一个东西。假设有字符串"ABABABB",那它的所有后缀为,以及后缀数组为:

     

  高度数组为所有后缀排好序之后的相邻两个后缀之间的最大公共前缀(LCP),比如height[1],看下标为1的后缀ABABB与上一个下标0的后缀ABABABB,最大公共前缀为ABAB,四个,那么height[1] = 4其余的也是一样,那么可以得到高度数组为height[] = {0,4,2,0,1,3,1}

  高度数组有一个重要规律就是:上一个下标i假如有k个公共前缀,并且k>0,那么下一个下标至少有一个k-1个公共前缀,那么前k个字符是不用比较的。

    static int[] getHeight(String src,Suff[] sa){
        // Suff[] sa = getSa2(src);
        int strLength = src.length();
        int []rk = new int[strLength];
        // 因为原来的sa数组是按照字符串相同排名相同,现在调整排名为不重复的排名,重新排名后得到数组rk。
        // 将rank表示为不重复的排名即0~n-1
        for (int i = 0; i < strLength; i++) {
            rk[sa[i].index] = i;
        }
        int []height = new int[strLength];
        // (存在的规律是上一个下标i假如有k个公共前缀,并且k>0,
        //  那么下一个下标至少有一个k-1个公共前缀,那么前k个字符是不用比较的)
        // 利用这一点就可以O(n)求出高度数组
        int k = 0;
        for(int i=0;i<strLength;i++){
            int rk_i = rk[i];  // i后缀的排名
            if (rk_i==0) {
                height[0] = 0;
                continue;
            }
            int rk_i_1 = rk_i - 1;
            int j = sa[rk_i_1].index;// j是i串字典序靠前的串的下标
            if (k > 0)
                k--;

            for (; j + k < strLength && i + k < strLength; k++) {
                if (src.charAt(j + k) != src.charAt(i + k))
                    break;
            }
            height[rk_i] = k;

        }
        return height;
    }

原文地址:https://www.cnblogs.com/xiaoyh/p/10322424.html

时间: 2024-10-28 22:58:16

字符串匹配(三)----后缀数组算法的相关文章

模式字符串匹配问题(KMP算法)

这两天又看了一遍<算法导论>上面的字符串匹配那一节,下面是实现的几个程序,可能有错误,仅供参考和交流. 关于详细的讲解,网上有很多,大多数算法及数据结构书中都应该有涉及,由于时间限制,在这就不重复了. 需要说明的是: stra:主串,及需要从中寻找模式串的字符串 strb:模式串 <算法导论>上面包括严蔚敏老师<数据结构>,字符串下表是按从1开始,并且<数据结构>一书中貌似吧字符串的第一个字符用来储存字符串长度.这里我改成了0. maxlen :字符串的最长

字符串匹配KMP next数组的理解

#include<cstdio> #include<cstring> void getNext(int *Next,char* src){ int i,j; Next[0]=-1; i=0; j=-1; int N=strlen(src); while(i<N-1){ if(j==-1||src[i]==src[j]){ ++i; ++j; Next[i]=j; }else{ /* 理解难点:假设已经存在Next:假设是两个字符串在进行比较. 1. a)现在有两个字符串 sr

字符串匹配问题【KMP算法】

一.问题 给定两个字符串S和T,找出T在S中出现的位置. 二.朴素算法 当S[i] != T[j]时,把T往后移一位,回溯S的位置并重新开始比较.    (1) 成功匹配的部分(ABC)中,没有一样的字符 S: i A B C A B C E T: j A B C E       S: i A B C A B C E T: j   A B C E     S: i A B C A B C E T: j     A B C E   S: i A B C A B C E T: j       A B

【BZOJ-1031】字符加密Cipher 后缀数组

1031: [JSOI2007]字符加密Cipher Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 5504  Solved: 2277[Submit][Status][Discuss] Description 喜欢钻研问题的JS同学,最近又迷上了对加密方法的思考.一天,他突然想出了一种他认为是终极的加密办法 :把需要加密的信息排成一圈,显然,它们有很多种不同的读法.例如下图,可以读作: JSOI07 SOI07J OI07JS I07JSO 0

[JSOI2007][BZOJ1031] 字符加密Cipher|后缀数组

1031: [JSOI2007]字符加密Cipher Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 4296  Solved: 1746[Submit][Status][Discuss] Description 喜欢钻研问题的JS 同学,最近又迷上了对加密方法的思考.一天,他突然想出了一种他认为是终极的加密办法:把需要加密的信息排成一圈,显然,它们有很多种不同的读法.例如下图,可以读作: JSOI07 SOI07J OI07JS I07JSO 0

BZOJ_1031_[JSOI2007]字符加密Cipher_后缀数组

Description 喜欢钻研问题的JS同学,最近又迷上了对加密方法的思考.一天,他突然想出了一种他认为是终极的加密办法 :把需要加密的信息排成一圈,显然,它们有很多种不同的读法.例如下图,可以读作: JSOI07 SOI07J OI07JS I07JSO 07JSOI 7JSOI0把它们按照字符串的大小排序:07JSOI 7JSOI0 I07JSO JSOI07 OI07JS SOI07J读出最后一列字符:I0O7SJ,就是加密后的字符串(其实这个加密手段实在很容易破解,鉴于这是 突然想出来

BZOJ 1031 JSOI 2007 字符加密Cipher 后缀数组

题目大意:给出一个字符串,循环字符串的起始位置,可以得到length个不同的字符串,问把这些字符串排序之后每一个字符串的第一位是什么. 思路:后缀数组裸题,只需要将整个字符串倍增,然后求一次sa. CODE: #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #define MAX 200010 using namespace std; char s[M

字符串匹配(KMP)算法及Java实现

一.什么是KMP算法? 维基百科的解释是:在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个主文本字符串S内查找一个词W的出现位置.此算法通过运用对这个词在不匹配时本身就包含足够的信息来确定下一个匹配将在哪里开始,从而避免重新检查先前已经匹配过的字符. 二.字符串的前缀与后缀 前缀:字符串除了最后一个字符的全部头部组合: 后缀:字符串处理第一个字符的全部头部组合:例如 三.字符串部分匹配表 "部分匹配"的实质是,有时候,字符串头部和尾部会有重

BZOJ 1031: [JSOI2007]字符加密Cipher 后缀数组

1031: [JSOI2007]字符加密Cipher Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 6014  Solved: 2503[Submit][Status][Discuss] Description 喜欢钻研问题的JS同学,最近又迷上了对加密方法的思考.一天,他突然想出了一种他认为是终极的加密办法 :把需要加密的信息排成一圈,显然,它们有很多种不同的读法.例如下图,可以读作: JSOI07 SOI07J OI07JS I07JSO 0