字符串匹配算法(RK算法和KMP算法)

算法1:RK算法

算法描述:

(1)计算模式串的Hashcode

方式1:按位相加;

方式2:看成26进制数转化为十进制,如abc = 1x26^2 + 2x26^1 + 4x26^0;

方式2缺点:字符串很长时,对应的十进制数会非常大

(2)主串采用增量计算

例如:主串:abbcefg;模式串bce

第一次计算abb,第二次计算bbc时:新Hahcode = 旧Hashcode - 'a' + 'c'

(3)检查 Hash Collision

//20200306
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN = 1000;
char pattern[MAXN],major[MAXN];
int hashcode(char* arr,int begin,int end){
    int sum = 0;
    for(int i=begin;i<end;i++){
        sum += (int)arr[i] - 'a' + 1; // ASCII of 'a' is 97
    }
    return sum;
}
int changehashcode(int sumori,int begin,int end){
    return sumori - major[begin-1] + major[end-1]; // +96-96 = 0
}
bool hashcollision(int begin,int end){
    bool state = false;
    for(int i=begin;i<end;i++){
        if(major[i] != pattern[i-begin]){
            state = true;
            break;
        }
    }
    if(state) return true;
    else return false;
}

int main(){
    int pos = -1;
    scanf("%s\n%s",pattern,major);
    int lenp = strlen(pattern);
    int lenm = strlen(major);
    int hashp = hashcode(pattern,0,lenp);
    int hashm = hashcode(major,0,lenp);
    if(hashm == hashp && !hashcollision(0,lenp)) return 0;
    for(int i=1;i<lenm-lenp;i++){
        hashm = changehashcode(hashm,i,i+lenp);
        if(hashm != hashp) continue;
        else if(hashcollision(i,i+lenp)) continue;
        else {
            pos = i;
            break;
        }
    }
    cout<<pos;
    return 0;
}
/*---Test---
bce
abbcefgh
//collision
bce
abcbefgh
*/ 
View Code

算法2:KMP算法

1.什么是next数组?

  next[i]就是子串s[0...i]的 最长 相等前后缀 的 前缀最后一位 的下标。(也是最长相等前后缀的长度

  什么是前缀?子串s[0...i]中s[0...k](k<i)为前缀,s[...i]为后缀,其中前后缀长度相等。也就是从0开始往后数k个,从最后一个字符开始往前数k个(k<i);(注意k!=i,即不能是子串本身)

举例:s[] = abababc

  当i=0时,子串为a,k<i=0中k无法取值,所以next[0] = -1;

  当i=1时,子串为ab, k=0 < i=1,前缀a,后缀b,不相等,

      所以next[1] = -1;

  当i=2时,子串为aba,k=0 < i=2, 前缀a,后缀a,相等,k=0

            k=1 < i=2,前缀ab,后缀ba,不相等,

      所以最长相等前后缀(即最大的k)为0,所以next[2] = k = 0;

  当i=3时,子串为abab,k=0 < i=3,前缀a,后缀b,不相等

              k=1 < i=3,前缀ab,后缀ab,相等,k=1

              k=2 < i=3,前缀aba,后缀bab,不相等,

      所以最长相等前后缀(即最大的k)为1,所以next[3] = k = 1;

  当i=4,5,6时,同理;

  当i=7(strlen(s) = 7)时,子串为abababc,k=0 < i=7,前缀a,后缀c,不相等。OK,这个时候k就不需要继续往下取值了,因为不可能再相等了,也就是说遇到不相等的情况就可以终止了。【相等k继续递增,不相等k立即终止,next[i]=k-1】;

  最终得到next数组为[-1,-1,0,1,2,3,-1]。

2.如何求next数组?(上面举例时采取的求next数组算法比较繁琐,故采用下述递推方法)

我们将在next[i-1]的基础上求next[i]:

  假设我们已经求出next[i-1],不妨令它等于j(也就是说s[0...i]的最长相等前后缀的长度为j),那么当我们继续读入一位时,最长相等前后缀的长度 在原来基础上 最多增加1位,也就是说,最长相等前后缀的长度最长为next[i-1]+1;

  (理解:字符串xyx的 最长相等前后缀长度为strlen(x),那么xyxa 最长相等前后缀长度 不可能超过strlen(xa) 并且 xyxa 最长相等前后缀长度等于strlen(xa)当且仅当a == y[0]

  也就是说,我们判断新读入的这个字符a与y[0]是否相等即可;y[0]是什么?它就是下标j+1所对应的字符,即读入该字符之前的那个字符串 最长相等前后缀的前缀最后一位的下标+1 作为下标对应的字符。

  如果 a == y[0],那么next[i] = j+1即可;

  如果a != y[0],该怎么办?

  直观的想法是从头(j = -1)开始匹配,这样的做法过于暴力,我们可以让j退回到最近的符合要求(a == s[j+1])的位置即可。也就是说:我们需要在字符串xyxa第一个x中找到尽可能靠后的字符a,假设我们把第一个x拆为waz,后一个x拆成uw,那么我们应该使字符串wazyuwa中z的长度尽可能小(w的长度尽可能大)。那么我们怎么找到waz中a的位置呢?现在,我们已知字符串z中最后一个字符的位置,该字符对应的next数组中所存值的含义是 字符串waz最长相等前后缀的前缀 最后一位的下标,不妨假设这个下标刚好对应字符a(如果不是我们循环地让j=next[j]即可,直到遇到a或到-1结束),那么字符串z就变成了wa,那么整个字符串wazyuwa中最长相等前后缀的长度就是strlen(wa)。

3.什么是KMP算法?

  我们发现,上述求next数组的过程,就是字符串“自匹配”的过程,也就是在给定字符串的子串s[0,1,2...i]中寻找s[0,1,...k](k<i)和s[.....,i]使得这两个子串完全相等并且k尽可能的大。那么同样的,字符串匹配问题是指给定一个模式串pattern,去和给定的主串text的子串匹配,寻找与模式串相匹配的子串,这个过程和模式串的next数组过程大同小异。

  因此,模仿求next数组的过程,我们从主串text和模式串pattern的第一个字符去匹配。如果相等,我们继续匹配text和pattern的第二个字符......如果还没有到pattern的末尾就遇到不相等的情况,这个时候我们要移动pattern位置了,如何一位一位地移动,那么就退化成BF算法了。那么究竟应该移到哪里呢?我们知道,模式串的next[i]数组记录的是模式串的子串pattern[0,1.....,i] 最长相等前后缀 的 前缀最后一位 的下标k,也就是说pattern[0,...k]和pattern[m,....,i](m = i-k)是一样的串。不匹配发生在pattern[i+1]和text[i+1],也就是说pattern[0,...,i]和text[0,....i]都匹配,那么pattern[m,...,i]和text[m,....i]匹配,那么pattern[0,...,k]和text[m,....i]匹配,那我们继续比对pattern[k+1]和text[i+1]是否相等就行了,而k+1就等于next[i+1]。

4.总结

next数组究竟有哪些含义?

(1)子串s[0...i]的 最长相等前后缀 的 前缀最后一位 的下标;

(2)子串s[0...i]的 最长相等前后缀的长度;

(3)主串与模式串匹配时,j+1位不能匹配时,j应该退回的位置。

5.KMP算法对应练习题洛谷OJ

AC代码:

//20200306
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN = 1000000;
char pattern[MAXN],text[MAXN];
int next[MAXN];
int value[MAXN];//store the result in LOGU OJ 
void getNext(){
    int len = strlen(pattern);
    next[0] = -1;
    int j = next[0];
    for(int i=1;i<len;i++){
        while(j!=-1 && pattern[i] != pattern[j+1]){
            j = next[j];
        }
        if(pattern[i] == pattern[j+1]) j++;
        next[i] = j;
    }
}
int KMP(){
    getNext(); 
    int lenpt = strlen(pattern);
    int lentx = strlen(text);
    int j = next[0];
    int ans = 0;
    for(int i=0;i<lentx;i++){
        while(j!=-1 && text[i] != pattern[j+1]){
            j = next[j];
        }
        if(text[i] == pattern[j+1]) j++;
        if(j == lenpt - 1){
            value[ans++] = i+1-j;
            j = next[j];
        }
    }
    return ans;
}
int main(){
    scanf("%s\n%s",text,pattern);
    int ans = KMP();
    for(int i=0;i<ans;i++) cout<<value[i]<<endl;
    int len = strlen(pattern);
    for(int i=0;i<len;i++) cout<<next[i]+1<<" "; //adjust to LOGU OJ
    
}
View Code

参考资料:

[1] 胡凡《算法笔记》P455~P464

猜你喜欢

转载自www.cnblogs.com/icodes8238/p/12301055.html