蓝桥杯算法基础(29)字符串匹配(RabinKarp)(KMP)(前缀树,字典树,trie,后缀数组,高度数组)

 RabinKarp

RabinKarp
S:ABABAB   m个
P:ABB      n个

1.朴素算法,挨个匹配
2.哈希法 hash->滚动哈希  c0*31^2+c1*31^1+c2类似于进制的求法求hash值(c0*31+c1)*31+c2
hash(p)=o(n)
hash(s)=o(m*n)

private static void match(String p,String s){
    long hash_p=hash(p);
    int p_len=p.length;
    for(int i=0;i+p_len<=s.length;i++){
    long hash_i=hash(s.subString(i,i+p_len));//i作为起点,长度为length的子串的hash值
    if(hash_i==hash_p){
    System.out.println("match:"+i);
    }
    }
}

final static long seed=31;
static long hash(String str){
    long hash=0;
    for(int i=0;i!=str.length;i++){//(c0*31+c1)*31+c2
    hash=seed*hash+str.charAt(i);
    }
    return hash%Long.MAX_VALUE;
}


每次求hash中间都相同的字符被计算

//n是子串的长度
//用滚动的方法求出s中长度为n的每个字串的hash求出hash数组
final static long seed=31;

static long[] hash(final String s,final int n){
    long[] res=new long[s.length()-n+1];
    //前n个字符的hash
    res[0]=hash(s.subString(0,n));
    for(int i=n;i<s.length;i++){//i的位置为新字符
     char newChar=s.charAt(i);
     char oldChar=s.charAt(i-n);
     //前m个字符的hash*seed-n个字符的第一个字符*d的n次方
     long v=(res[i-n]*seed)-NExponent.ex2(seed,n)*oldChar+newChar)%Long.MAX_VALUE;;
        res[i-n+1]=v;
    }//NExponent.ex2(seed,n)自己封装的工具,求seed的n次方

    return res;
}


static long hash(String str){
    long h=0;
    for(int i=0;i!=str.length;i++){
    h=seed*h+strcharAt(i);
    }
}

 KMP

KMP
1.暴力匹配
private static int indexOf(String s,String p){
    int i=0;
    int sc=i;
    int j=0;
    while(sc<s.length()){
     if(s.charAt(sc)==p.charAt(j)){
        sc++;
        j++;
        if(j==p.length)
        return i;
    }else{
        i++;
        sc=i;//扫描指针以i为起点
    }

    }
}


2.next数组的含义及应用
后缀与前缀的最长匹配长度
f(L)=k,L失配,s回溯k
不考虑第L个元素
P[0~L-1]的pre, suff最长匹配数
next数组
b a b a b b
-1 0 0 1 2 3
取该位next数组,应将该为元素排除在外
取下标为0的next,为-1,因为将排除在外
取下标为1的next,为0,将a排除在外,b没有前缀没有后缀,因为我们不包含它的整体
前两个next一般都为-1,0
取下标为2的next,为0,排除b,b a前缀后缀匹配为0
取下标为3的next,为1,排除a,b a b前缀后缀匹配为1,前缀b和后缀b匹配
如此取出所有next


3.求next数组
private static int indexOf(String s,String p){
    if(s.length()==0||p.length()==0)return -1;
    if(p.length()>s.length())return -1;//模式串大于原串的长度

    int count=0;
    int[] next=next(p);
    int i=0;//s位置
    int j=0;//p位置
    int sLen=s.length();
    int pLen=p.length();
    while(i<sLen){
    //如果j=-1,或者当前字符串匹配成功,即S[i]==p[i],则令i++,j++
    //j=-1,因为next[0]=-1,说明p的第一位和i这个位置无法匹配,这时i,j都增加1,i移位,j从0开始
    if(j==-1||s.charAt(i)==p.charAt(j)){
        i++;
        j++;
    }else{
    //如果j!=i,且当前字符匹配失败,即S[i]!=P[j],则令i不变,j=next[j]
    //next[j]即为j所对应的next数组
    j=next[j];

    }
    if(j==plen){//匹配成功了
    count++;
    j=next[j];//求匹配成功的次数,让i继续向下走
   // return (i-j);
        }
    }
    return count;//返回匹配成功的次数
    //return -1;

}

//求next数组
j,k为上字符是否相同
如果pj==pk,next[j+1]=k+1或者k<0,next[j+1]=k+1;
否则k继续回溯,直到满足pj==pk或k<0;
//若是有一个相同,则下一个位置即j+1的next为k+1,
//即前缀后缀有一个成功匹配,到达下一个位置,
//再看当前位置+1后j的元素,与+1后的k的相比较,
//若是相同,则j+1位置的next为K+1,则证明有两个成功匹配的长度,
//若是下一个位置j与k位置的元素不同,则之前成功匹配的则作废,
//则k回溯找前面相同的继续前缀后缀匹配,
//若是找不到,即k等于-1,则下一个位置的next为0,相当于-1+1=0
private static int[] next(Strig ps){
    int pLength=ps.length();
    char[] p=ps.toCharArray();
    next[0]=-1;
    if(ps.length()==1){
    return next;
    }
    next[1]=0;//初始化next[0]=-1,next[1]=0

    int j=1;
    int k=next[j];//看看位置j的最长匹配前缀在哪里

    while(j<plength-1){
    //现在要推出next[j+1],检查j和k位置上的关系即可
        if(k<0||p[j]==p[k]){//当k回溯到-1或是p[j]和p[k]相等时
            next[++j]=++k;
         }
         else{
         k=next[k];//k回溯
         }
    }

    return next;

}

前缀树(字典树,Trie);后缀数组

1.什么是后缀数组
就是串的所有后缀子串的按字典序排序后,在数组中记录后缀的起始下标,

后缀数组就是:排名和原下标的映射,Sa[0]=5,起始下标为5的后缀在所有后缀中字典序最小

rank数组:给定后缀的下标,返回字典序,rk[5]=0;rk[sa[i]]=i

2.后缀数组有什么用

 子串:一定是某个后缀的前缀

 private static void match(){
    String s="ABABABABB";
    String p="BABB";
    Match03_SuffixArray.Suff[] sa=Match03_SuffxiArray.getSa2(s);//后缀数组
    int l=0;
    int r=s.length()-1;
    //二分查找。nlog(m)
    while(r>=l){
    int mid=l+((r-l)>>1);
    //居中的后缀
    Match03_SuffixArray.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);
        //若是某后缀长度大于子串的长度,则截取后缀前面子串长度的部分与子串比较
        //若是后缀大于子串,则为正,r=mid-1
        //若是后缀小于子串,则为负,l=mid+1
    }else{
        compareRes=suffstr.compareTo(p);//后缀长度小于p,若是相同则p更大,因为字符串更长的更大,若是不同,则按字典序正常比较大小
    }

    if(compareRes==0){
    System.out.println(midSuff.index);
    break;
    }else if(compareRes<0){
    l=mid+1;
    }else{
    r=mid-1;
    }

 }

ABABABABB
ABABABABB-0
 BABABABB-1
  ABABABB-2
   BABABB-3
    ABABB-4
     BABB-5
      ABB-6
       BB-7
        B-8

ABABABABB-0  ABABABB-2 ABABB-4.....suffix array sa


3.怎么求后缀数组
  3.1 把所有的后缀放入数组,Arrays.sort();nlog(n)


//直接对所有后缀数组排序,因为字符串的比较消耗o(n)
//所以整体为n^2*log(n)
public static Suff[] getSa(String src){
        int strLength=src.length();
        //sa是排名到下标的映射,即sa[i]=k说明排名为i的后缀是从k开始的
        Suff[] suffixArray=new Suff[StrLength];
        for(int i=0;i<strLength;i++){
        String SuffI=src.substring(i);//substring截取字符串,此字符串为一个子字符串,从索引位置的字符开始,直到字符串末尾

        suffixArray[i]=new Suff(suffI,i);//将该子字符串与所处位置放入后缀数组
        }
        Arrays.sort(suffixArray);//快排
        return suffixArray;
}

//面向对象,类似与c语言结构体
static class Suff implements Comparable<suff>{//Comparable接口的约定,必须实现compareTo()
     String str;//后缀内容
     int index;//起始下标


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

     }

     public int campareTo(Suff o2){
     return this.str.compareTo(o2.str);

     }


     public String toString(){
     return "Suff{"+"str'"+str+'\''+",index="+index+"}";
     }

}


 倍增法

3.1
1.suff[] A~0,B~1,A~1,B~3,A~4,B~5,A~6,B~7,B~8
2.Sort: A~0,A~2,A~4,B~1,B~3,B~5,B~7,B~8
3.rk r[0]-0,r[2]=1,r[4]=0,r[6]=0,r[1,3,5,7,8]=1给我下标,给你排名

1.suff[] AB~0,BA~1,AB~1,BA~3,AB~4,BA~5,AB~6,BB~7,B~8
2.Sort: A~0,A~2,A~4,B~1,B~3,B~5,B~7,B~8
3.rk r[0]-0,r[2]=1,r[4]=0,r[6]=0,r[1,3,5,7,8]=1给我下标,给你排名





int[] rk=new int[Strlength];//suffix array
Suff[] suffixArray = new Suff[strlength];
    for(int k=1;k<=strlength;k*=2){
        for(int i=0;i<strlength;i++){//k代for(int i=0;i<strlength;i++){//k代表取值范围,一开始只截一个字符参与排序,下次两个字符参与排序,依次乘2}
        String suffI=src.substring(i,i+k>strlengthl?strelngth:i+k);
        suffixArray[i] =new Suff(suffI,i);
        }

        if(k==1)//一个字符,直接排序
        Arrays.sort(suffixArray);//nlog(n)
        else{
        //填充完毕,开始排序nlog(n)
        final int kk=k;
        Arrays.sort(suffixArray,(o1,o2)->{//简化匿名内部类,定义一个比较器
        //不是基于字符串比较,而是利用之前的rank
        int i=o1.index;
        int j=o2.index;
        if(rk[i]==rk[j]){//如果o1和o2的前半段
        try{//利用上一级排名给下一级排名
        //无论是rk[i]还是rk[i+kk/2]在上一级排名中都是有的
             return rk[i+kk/2]-rk[j+kk/2];//若是有一个后缀字符串较短。+kk/2就会越界
            //x x
            //i i+k/2
            //Y Y
            //j j+k/2
        }catch(ArrayIndexOutOfBoundsExcept e){//越界
        return o1.str.length()-o2.length();//前半部分一致,谁长谁在后
        }
        }else{//若不相等,则排名大的在后
        return rk[i]-rk[j];
        }
        });

        }



int r=0;
rk[suffixArray[0].index]=r;
for(int i=1;i<strlen;i++){
    if(suffixArray[i].compareTo(suffixArray[i-1]==0)){//内容相同
    rk[suffixArray[i].index]=r;//索引->排名,给定索引,知道单个字符的排名

    }else{
    rk[suffixArray[i].index]=++r;//索引->排名,给定索引,知道单个字符的排名

            }
        }
    }
      return suffixArray;
}
private static void match(){
String s="ABABABABB";
String p="BABB";

Match03_SuffixArray.Suff[] sa=Match03_SuffixArray.getSa2(s);//后缀数组
int l=0;
int r=s.length();
//二分查找
while(r>=l){
    int mid=l+((r-l)>>1);
    //居中的后缀
    Match03_SuffixArray.Suff midSuff=sa[mid];
    String suffstr=midSuff.str;
    int compareRes;
    //将后缀和模式串比较o(n)
    if(suffstr.length()>=p.length()){

    compareRes=suffix.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;
    }
}


}

3.2倍增法
    k=1,一个字符,排序,得到sa,rk
    k=2,利用上一轮的k快速比较两个后缀
    k=4
    k=8
    ....

public static Suff[] getSa2(String src){
    int n=src.length();
    Suff[] sa=new Suff(n);
    for(int i=0;i<n;i++){
     sa[i]=new Suff(src.charAt(i),i);//有单个字符,接下来排序

    }
    Arrays.sort(sa);
}

        //rk是下标到排名的映射
       int[] rk=new int[n];//suffix array
        rk[sa[0].index]=1;
        for(int i=1;i<n;i++){
        rk[[sa[i].index]=rk[sa[i-1]].index;
        if(sa[i].c!=sa[i-1].c)rk[sa[i].index]++;
        }

        //倍增法
        for(int k=2;rk[sa[n-1].index]<n;k*=2){

            final int kk=k;
            Arrays.sort(sa,(o1,o2)->{
            //不是利用字符串比较而是利用之前的rank
            int i=o1.index;
            int j=o2.index;
            if(rk[i]==r[j]){//如果第一关键字相同
            if(i+kk/2>=n||j+kk/2>=n)
            return  -(i-j);//如果某个后缀不具有第二关键字,那肯定较小,索引靠后的更小
           return rk[i+kk/2]-rk[j+kk/2];
            }else{
            return rk[i]-rk[j];

              }

            });
            //排序,end
            //更新rank
            rk[sa[0].index]=1;
            for(int i=1;i<n;i++){
            int i1=sa[i].index;
            int i2=sa[i-1].index;
            rk[i1]=rk[i2];
            try{
              if(!src.subString(i1,i1+kk).equals(src.subString(i2,i2+kk)))
              rk[i1]++;
            }catch(Exeption e){
                   rk[i1]++;
            }
            }

        }


高度数组

height[0]=0;
height[i]=Lcp(sa[i],sa[i-1])
               串      串
//Lcp最长公共前缀

0 ABCABC      0 ABC(排序后)
1  BCABC      1 ABCABC
2   CABC      2 BC
3    ABC      3 BCABC
4     BC      4 c
5      c      5 CABC
rk[0]=1   0 ABCABC-->1 ABCABC
sa[1]=0   1 ABCABC-->0 ABCABC
sa[rk[0]]=0
rk[sa[1]]=1
根据排名,求的当前串的位置和前一个串的位置,再根据位置求得两个串,在比对两个串求得最长公共前缀

//height[rk(i-1)]->height[rk(i)]
//height[rk(i)]->height[rk(i+1)]
0 ABC        2 BC
1 ABCABC     3 BCABC
就是抹了个帽子A,最长公共前缀-1
// height[rk(i+1)]>=height[rk(i)]-1

public static int[] getHeight(String src,Suff sa){
    //Suff[] Sa=getSa2(src);
    int strlength=src.length();
    int[] rk=new int[Strlength];
    //将rank表示为不重复的排名即0-n-1
    for(int i=0;i<strLength;i++){
    rk[Sa[i].index]=i;//index为原始的位置,i为排序后的位置
    }

    int[] height=new int[StrLength];
    //如果已经知道后缀数组中i与i+1的lcp为h,那么i代表的字符串与i+1代表的字符串去掉首字母的lcp为h-1
    //根据这个我们可以发现,如果知道i与后缀数组中在它后一个的lcp为k,那么它去掉首字母后的字符串与其在后缀数组中的后一个的lcp大于等于k-1
    //例如对于字符串abcefabc,我们直达票abcefabc与abc的lcp为3
    //那么bcefabc与bc的lcp大于等于3-1
    //利用这一点就可以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;//height[0]默认为0
    continue;
    }
    int rk_i_1=rk_i-1;//前一个位置
    int j=sa[rk_i_1].index;//j是i串字典序靠前的串的下标
    if(k>0)k--;//利用height[rk(i+1)]>=height[rk(i)]-1,下一次只是去掉了一个帽子的公共前缀
    for(;j+k<strLength&&i+k<strLength;k++){
        if(src.charAt(j+k)!=src.charAt(i+k))
        break;
    }
    height[rk_i]=k;//i是一个个递增的来的,若是这个方法是错误的,应该一个一个比对公共前缀
    }
    return height;


}

相关推荐

  1. 每日一题(Tire字典

    2024-03-26 15:52:03       19 阅读
  2. Python高级数据结构——字典Trie

    2024-03-26 15:52:03       42 阅读
  3. 数据结构(Trie字典))

    2024-03-26 15:52:03       5 阅读
  4. Tire 字典前缀

    2024-03-26 15:52:03       11 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-26 15:52:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-26 15:52:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-26 15:52:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-26 15:52:03       18 阅读

热门阅读

  1. WPF 界面命令绑定(MVVM结构)

    2024-03-26 15:52:03       17 阅读
  2. flutter 父组件调用子组件方法

    2024-03-26 15:52:03       16 阅读
  3. Docker

    Docker

    2024-03-26 15:52:03      17 阅读
  4. 45个常用Linux 命令,让你轻松玩转Linux

    2024-03-26 15:52:03       17 阅读
  5. 【保姆级讲解深度学习如何入门?的教程】

    2024-03-26 15:52:03       17 阅读
  6. 为什么要学编程?

    2024-03-26 15:52:03       16 阅读
  7. 使用 Docker Compose 部署单机版 Redis

    2024-03-26 15:52:03       15 阅读