字符串处理 —— 字符串匹配问题 —— KMP 算法

【概述】

KMP 算法又称 Knuth-Morris-Pratt 字符串匹配算法,是由于 D.E.Knuth、J.H.Morris 和 V.R.Pratt 三人共同研究的,用于解决字符串匹配问题。

其核心思想是利用已经部分匹配的有效信息,来保持文本串指针不回溯,通过修改模式串指针,让模式串尽量地移动到有效的位置,因此,整个 KMP 的重点就在于当模式串中某个字符与文本串不匹配时,如何移动模式串指针。

KMP 算法的主要特点是:

  1. 需要对模式字符串做预处理;
  2. 预处理阶段需要额外的 O(m) 空间和复杂度;
  3. 匹配阶段与字符集的大小无关;
  4. 匹配阶段至多执行 2n-1 次字符比较;
  5. 对模式中字符的比较顺序时从左到右;

【算法原理】

如下图,以文本 T=ABACBCDHIJK,模式 P=ABAD 为例,前面三个字符是匹配的而 C 与 D 不匹配,可以利用已匹配的信息将模式指针 j 移到第 1 位上(指针从 0 开始),文本指针 i 位置不变

同理,对于下图中的文本 T=ABCABCDHIJK 与模式 P=ABCABB,当 C 与 B 不匹配时,可利用已匹配信息,将模式指针 j 移动到第 2 位上,文本指针 i 位置不变

通过上面两个例子,可以看出,当匹配失败时,文本指针 i 的位置不变,模式指针 j 要移动到位置 k,而位置 k 的性质满足:模式串最前面 k 个字符与 j 之前的最后 k 个字符相同,即:P[0\sim k-1]=P[i-k\sim j-1]

当 T[i]!=P[j] 时,有:T[i-j \sim i-1]=P[0 \sim j-1]

根据P[0\sim k-1]=P[i-k\sim j-1]

可得:T[i-k\sim i-1]=P[0\sim k-1],因此可以判断直接将 j 移动到 k 无须再比较前面的 k 个字符

至此,关键的部分是求得 k,由于在模式 P 的每一个位置都可能发生不匹配,也就是说需要计算每一个位置 j 对应的 k,故而可以用一个数组 next[] 来保存,即令 next[j]=k 表示当 T[i]!=P[j] 时,j 指针的下一个位置

以下图为例,可以发现,当 j 为 0 时就不匹配,这个时候 j 已经在最左边了,不可能进行移动,需要调整的是将指针 i 后移一位,因此要对 next[] 数组初始化,即 next[0]=-1

再以下图为例,当 j=1 时不匹配,因为他前面只有一个位置,那么显然指针 j 需要移到 0 位置

对比以下两个图,可以发现,当 P[k]=P[j] 时,有 next[j+1]=next[j]+1

证明:

由于 P[j] 之前已经有 P[0\sim k-1]=P[i-k\sim j-1],即 next[j]=k

这个时候有 P[k]=P[j],那么可以得到 P[0 \sim k-1] + P[k] = P[j-k\sim j-1] + P[j]。

即:P[0 \sim k] = P[j-k \sim j],即:next[j+1] = k + 1,从而有:next[j+1]=next[j]+1

而当 P[k] \:\:!=P[j] 时,对比以下两张图,可以发现只要令 k=next[k] 即可

此时,next 数组已经求出,但还存在一个缺陷

以下图为例,显然得到的 next 数组是 {-1,0,0,1}

如下图,当 C 与 B 不匹配时,应将模版指针 j 移动到第一个元素

不难发现,由于后面的 B 已经不匹配了,那么前面的 B 也一定是不匹配的,因此这一步完全没有意义,同理,A 也一样

显然,发生问题的原因在于 P[j]=P[next[j]] ,因此加一个判断条件即可,值得注意的是,何时需要加上此条判断要根据实际情况

【应用】

KMP 算法的核心是求 next 数组,最大的应用除解决字符串匹配问题外,常用 next 数组来求循环节。

在未加判断的 next 数组中,其代表当前字符之前的字符串中,最大前缀与后缀匹配数,即 next[j]=k 代表 j 之前的字符串中有最大长度为 k 的相同前缀后缀。

例如:

i 0 1 2 3 4 5
p[i] a

b

c b a  
next[i] -1 0 0 0 0 1

对于长度为 n 的模式串,由于 next[j]=k 表示 p[1...i-1] 最大前缀与后缀匹配数,那么模式串第 1 位到 next[n] 位与模式串第 n-next[n] 位到第 n 位是匹配的

因此当 next[i]>0 时,i-next[i] 为字符串匹配的时候移动的位数,故而当 n%(n-next[n])=0 时,说明模式串中存在重复连续的子串,其长度为 len=n-next[n],那么字符串的最小周期 res=n/len=n/(n-next[n])

【算法实现】

1.求 next 数组

1)不加判断版

int next[N];
void getNext(char p[]){
    next[0]=-1;//初始化

    int len=strlen(p);//模式串长度
    int j=0;//模式指针j
    int k=-1;//位置k

    while(j<len) {
        if(k==-1||p[j]==p[k]) {//next[j+1]=next[j]+1
            k++;//此前有next[j]=k
            j++;//指针后移
            next[j]=k;
        }else{
            k=next[k];
        }
    }
}

2)加判断版

int next[N];
void getNext(char p[]){
    next[0]=-1;//初始化

    int len=strlen(p);//模式串长度
    int j=0;//模式指针j
    int k=-1;//位置k

    while(j<len) {
        if(k==-1||p[j]==p[k]) {//next[j+1]=next[j]+1
            k++;//此前有next[j]=k
            j++;//指针后移
            if(p[j]==p[k])//当两个字符相等时跳过
                next[j] = next[k];
            else
                next[j]=k;
        }else{
            k=next[k];
        }
    }
}

2.匹配

int next[N];
int KMP(char t[],char p[]) {
    int tLen=strlen(t);//文本串长度
    int pLen=strlen(p);//模式串长度

    int i=0;//文本串指针
    int j=0;//模式串指针

    getNext();//获取next数组

    while(i<tLen&&j<pLen) {
       if (j==-1||t[i]==p[j]){//当j为-1时,要移动的是i,同样j也要归零
           i++;
           j++;
       }else{
           j=next[j];//j回到指定位置
       }
    }

    if(j==pLen)//最终当模式串的位置与模式串的长度相同时,说明匹配成功
       return i-j;
    else//匹配失败
       return -1;
}

3.求最小循环节长度

int next[N];
void getNext(char p[]){
    next[0]=-1;//初始化

    int len=strlen(p);//模式串长度
    int j=0;//模式指针j
    int k=-1;//位置k

    while(j<len) {
        if(k==-1||p[j]==p[k]) {//next[j+1]=next[j]+1
            k++;//此前有next[j]=k
            j++;//指针后移
            next[j]=k;
        }else{
            k=next[k];
        }
    }
}
int main(){
    scanf("%s",p);
    int n=strlen(p);//模式串长度
    getNext();//获取next数组
    
    int len=n-next[n];//与前缀相同的后缀长度
    if(n%len==0){//存在循环节
        int res=n/len;//循环节长度
        printf("%d\n",res);
    }
    else//不存在循环节,长度为1
        printf("1\n");
    return 0;
}

猜你喜欢

转载自blog.csdn.net/u011815404/article/details/87937177