5.3 KMP算法
smile
//我们把一个串的结构定义成这个样子,用malloc去给他一定的空间存储字符串
typedef struct{
char* str;//字符串
int len;//字符串长度
}Str;
朴素算法:
int foo(char a[],char b[]){
int m=strlen(a);
int n=strlen(b);
int i,j;
i=j=0;
int tem=i;
while (i < m && j < n){
if (a[i]==b[j]){
i++;
j++;
}
else{
j=0;
i=++tem;
}
}
if (j==n){
return tem;
}
return -1;
}
观察上面的算法,这就是传说中的朴素算法.确实很朴素,我们将模式串的从第一位开始与母串逐位进行比较,每当发现有一个字符不能够match,我们就又开始从"与模式串头部对应的母串字符的下一位"开始,将模式串与母串逐位进行比较,以此类推,直到找到与模式串相匹配的子串,如果找到返回子串首字符的下标,否则返回-1.
这样做确实能寻找到一个substr ,and the substr is match with the pattern.But we has wasted many times,because 我们做了一些注定不会匹配的比较.比如说有一个母串:a b c a b x a b c a b y,and a pattern:a b c a b y,when we first compare the first one with second one,当比较到下标为5的为位置时我们会发现它们不能够匹配,这时候我们就会发现下一次使用到母串的下标5位置的字符与pattern中相应字符进行比较的时候,才是我们真正需要的比较,而中间的过程都是一种浪费时间的行为.而避免这种浪费就是KMP算法所要做的事.
假如说我们现在有一个array,里面存储着一组数,这些数的数目与pattern的长度相同,并且他们的下标与pattern的每一个字符的下标一一对应,我们先给这个array起个名字,叫next.每一次遇到与母串的字符不匹配的pattern字符时,我们把对应字符的下标对应next值取出来,看这个值要求我们现在应该把pattern的第几个字符继续进行上面的匹配操作(要解释为什么要这样移动的原因,我们要引入前缀和后缀的概念,前缀就是字符串中除了最后一个字符,所有包含第一位字符的连续子串,后缀同理,我们给next[i]赋值为前i-1个字符所组成的字符串最长匹配前后缀的长度加上一,仔细想一想,如果此时进行粗体字所说的操作,就相当于把pattern的前缀对应的放到了原来后缀所在的位置上,我们不必再做相当于此时前缀长度那么多次数的比较就能再次去解决原来的不匹配了,积少成多)如果匹配上了这一位,继续进行下一位的匹配,如果还不匹配,依次类推,直到连pattern里面的第一位都不能与这个字符匹配,那么我们就去不管这个母串字符了,去拿下一个母串字符与pattern首字符进行匹配,直到找到相应的子串,这种算法就叫做KMP算法.
用代码实现如下:
int KMP(Str str,Str substr,int next[]){
int i=0;
int j=0;
GetNext(substr,next);
while (i < str.len && j < substr.len)
{
if (j==-1||str.str[i]==substr.str[j])//逐位比较,如果相等则进行下一位
{
i++;
j++;
}else{
j=next[j];//如果某一位不能匹配,找到一个最近的可能与它匹配的字符
}
}
if (j==substr.len)//如果找到的话
{
return i-substr.len;//返回相应下标
}
return -1;//找不到返回-1,代表没有这样一个substr
}
那么现在的问题就是如何得到我们需要的next数组了,我们直接在代码中解释得到next数组的过程
void GetNext(Str substr,int next[]){
next[0]=-1;//我们先将数组的第一个元素设置为-1,一会儿再来解释这样做的好处
int j=0;
int t=next[0];
//这里我们把substr复制成两个,分别放在上下两个位置,j对应上面这个副本,t对应下面这个副本
while (j<substr.len-1)
{
if (t==-1||substr.str[j]==substr.str[t])
{
t++;
j++;
next[j]=t;
//在第一次循环中,一定会把next[1]赋值为0,很明显,任何字符串,它的下标小于1的字符都只有一个,那么自然最长的匹配的前后缀的长度就是0了.(接下来的两行注释均不针对第一次循环)
//如果是因为第一个条件进入了是因为上一次的比较中在下面的pattern副本与上面的副本的匹配过程中,连第一位字符无法与j所对应的字符匹配,说明相应的前缀与后缀的长度只能为0,那么自然也需要让j++,即跳过j所对应的这一位,并且将此时的next[j]赋值为t(此时t一定等于0)
//如果是因为满足第二个条件进入了循环体,那么相当于此时对应的pattern的子串的前缀和后缀的长度各加长了一位,也就是匹配长度加长了一位
}else{
t=next[t];
//如果程序能走到这个语句,那么说明此时的下标j和t处的字符不匹配,并且不是在用首字符进行匹配,那么我们就把t下标所对应的next值赋给t,t一定是小于j的,而我们此时已经得到了j以前的next值,所以这样做是没有问题的,这样做的原理与我们在KMP算法中应用next数组的原理是相同的
}
}
}
这样我们就介绍完了大名鼎鼎的KMP算法,实在是花了很长时间,but to me,it’s valuable.