KMP模式匹配算法

1. 从朴素的模式匹配算法讲起

基本问题:从主串S中,找到T这个子串的位置。

例如:主串S:“goodgoole”,子串T:“goole”,得到结果T在S出首次出现的位置在S起始字符开始第五个字符。

这种子串定位问题称为“串的模式匹配”

朴素算法思路最简单,就是每次从S的第i个字符开始比较,判断S中第i+j字符是否与T中第j个字符相等,相等的话j++,直到i+j超过S的长度或者j超过T的长度,然后返回(i+j超过S的长度:返回-1,表示主串中不存在子串;j超过T的长度:返回i,表示主串中子串的初始位置为i)。

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int index(char* S, char* T, int pos);
int main(void){
    //构造主串和子串
    const char* str1="goodgoole";
    const char* str2="goole";
    char* S=(char*)malloc(sizeof(char)*100);
    char* T=(char*)malloc(sizeof(char)*10);
    S[0]=strlen(str1);//串的第一个位置记录串的长度
    T[0]=strlen(str2);
    strcpy(&S[1],str1);//字符串拷贝
    strcpy(&T[1],str2);
    int idx=index(S,T,1);
    printf("index:%d\n",idx);
    return 0;
}
//返回子串T在主串S中第pos个字符之后的位置。若不存在,函数返回0
//T非空,1<=pos<=S[0]
int index(char* S, char* T, int pos){
    int i=pos;  //i用于记录主串S当前位置下标
    int j=1;    //j用于记录子串T当前位置下标
    while(i<=S[0]&&j<=T[0]){//当i+j小于S的长度并且j小于T的长度时,循环执行
        if(S[i]==T[j]){//主串和子串当前位置字符相等时,将i,j下移一位
            i++;
            j++;
        }else{//不等时,i回溯,j退回到T的第一个字符
            i=i-j+2;
            j=1;
        }
    }
    if(j>T[0])//说明找到了完整的子串,返回其首字符出现的位置
        return i-T[0];
    else
        return 0;//否则返回0,表示没有找到
}

while循环中的代码也可以写成这样:

    while(i<=S[0]&&j<=T[0]){//当i+j小于S的长度并且j小于T的长度时,循环执行
        if(S[i+j-1]==T[j]){//主串和子串当前位置字符相等时,将j下移一位
            j++;
        }else{//不等时,i回溯,j退回到T的第一个字符,i++,表示i前进一个字符
            i++;
            j=1;
        }
    }

    if(j>T[0])//说明找到了完整的子串,返回其首字符出现的位置
        return i;
    else
        return 0;//否则返回0,表示没有找到

两种代码的功能都是一样的,只不过表示方式略有区别。将i和j同时进行“++”操作,然后回溯i,这种表示下,i时刻都表示当前主串中比较的字符的位置;而另一种表述中i只用来记录当前比较中,主串中开始进行比较的初始位置,每次比较不成功的话,初始位置下移一个字符。

其实通过“回溯”的方法,我们可以大概有个印象,每次整个子串的比较不成功时,主串中比较的位置先回溯到上一次比较的初始位置,然后在这个初始位置的基础上,后移一个字符。这种方法的回溯过程,使得比较的效率降低了,最极端的情况,比如S=“00000000000000000000001”,T=“0001”,这种朴素的方法需要比较失败很多次,然后回溯很多次,最后才能找到正确的位置。

但是我们看S=“00000000000000000000001”,T=“0001”,这种情况,当第一次比较失败的时候,i=4,j=4,此时按照回溯的方法,下一次应该从i=2,j=1的地方开始比较,但是实际上,由于我们比较了S前三个字符跟T前三个字符相等,同时T的前三个字符是相同的,因此我们可以直接从i=4的地方继续,只不过这时候比较的是子串中j=3的位置。

2. KMP模式匹配算法

三位前辈,D.E.Knuth, J.H.Morris和V.R.Pratt(其中Knuth和Pratt共同研究,Morris独立研究)发表了一个模式匹配算法,可以大大避免重复遍历的情况。

以上面的图为例,当比较到ij的时候,发现当前位置的字符不相等,按照朴素算法,此时应该进行回溯,i=i-j+2,j=1,如上图所示。但是考虑到实际T串中可能会有重复的字符,如下图所示,假设T串中绿色部分的字符是相等的,那么,经过之前的比较,已经确保了A2区域的字符是相等的,又因为子串T中绿色字符是相等的,因此,当判断到此时i和j位置上的字符不相等之后,可以直接将子串T向后移动,跳过A1区域,直接比较i和子串中j=4的字符是否相等。

我们人为创造一个Next数组,用于存储子串T中第k(1<=k<=T[0])个字符前面的字符串中首尾中相等字符的个数(实际存的是相等字符个数+1)。比如上面的图中,Next[j]=4,表明j前面的j-1个字符中,前(Next[j]-1)个字符和后(Next[j]-1)个字符相等。有了Next数组之后,i不用回溯,而是直接根据Next[j]的值,将子串T后移,从中间开始比较即可。如下图:

到这里可能会有疑问,j前面的字符中首尾字符都不相等,比如“abcdef”,这样的话,我们人为规定Next[j]=1,下一次判断从子串的首部开始。同时因为i前面j次判断是相等的,而子串Tj前面的字符都不相等,那么可以说明即使i回溯到i-j+2,也不会在j-1个中字符中找到跟T[1]相等的字符因此我们不改变i,而是回退j(相当于将子串T向后移动)

Next数组可以根据下式定义:

                         

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int index(char* S, char* T, int pos);
int main(void){
    //构造主串和子串
    const char* str1="goodgoole";
    const char* str2="goole";
    char* S=(char*)malloc(sizeof(char)*100);
    char* T=(char*)malloc(sizeof(char)*10);
    S[0]=strlen(str1);//串的第一个位置记录串的长度
    T[0]=strlen(str2);
    strcpy(&S[1],str1);//字符串拷贝
    strcpy(&T[1],str2);
    int idx=index(S,T,1);
    printf("index:%d\n",idx);
    return 0;
}

void getNext(char* T,int* next){
    int i,j;
    i=1;
    j=0;
    next[1]=0;
    while(i<T[0]){  //T[0]表示T串的长度
        if(j==0||T[i]==T[j]){//T[i]表示后缀的单个字符,T[j]表示前缀的单个字符
            i++;
            j++;
            next[i]=j;
        }else j=next[j];//若字符不相同,j值回溯
    }
}

//返回子串T在主串S中第pos个字符之后的位置。若不存在,函数返回0
//T非空,1<=pos<=S[0]
int index(char* S, char* T, int pos){
    int i=pos;  //i用于记录主串S当前位置下标
    int j=1;    //j用于记录子串T当前位置下标
    int next[10];//定义next数组
    getNext(T,next);//得到next数组的值
    while(i<=S[0]&&j<=T[0]){//当i+j小于S的长度并且j小于T的长度时,循环执行
        if(j==0||S[i]==T[j]){//增加了j==0的判断
            i++;
            j++;
        }else{//不等时,j退回到next[j],i不变
            j=next[j];
        }
    }
    if(j>T[0])//说明找到了完整的子串,返回其首字符出现的位置
        return i-T[0];
    else
        return 0;//否则返回0,表示没有找到
}

参考资料:《大话数据结构》程杰【著】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值