kmp算法我以前写过一篇数学推导原理的文章,但是很难让人看下去。最近想到了另一种解释,原理超级简单。
如图:
主串和模式串一直到主串的k才和模式串不匹配。
看图可知:
因为:
x2=y2
又:
y1=y2
所以:
x2=y1
所以下一次模式串匹配就可以直接拿模式串的y1段和主串的x2段对齐,然后比较之后的字符串了。
这就省掉了暴力算法的很多步。所以kmp算法要去寻找模式串的最大公共前后缀。
这就是kmp算法的原理,如何,是不是很简单。它其实就是充分利用了匹配成功的部分产生的信息。
那么,如何求模式串的最大公共前后缀呢?
要求模式串的最大公共前后缀,就要引入next数组
next数组的定义:当主串与模式串的某一位字符不匹配时,模式串要回退的位置的数组下标。
i:模式串正在进行匹配的字符的数组下标
next[i]:模式串第i+1个字符不匹配时,模式串要回退的位置的数组下标
next[i]=第(i+1)个字符前面 i 位字符组成的字串的前后缀重合字符串
拿之前的两个图来说明一下:
这里到y2那个地方前面的字串的前后缀重合字符数为3
到y2那个地方前面的字串的前后缀重合字符数为3,所以模式串回退到第4个字符再与主串刚匹配错误的字符开始比较。而数组下标从0开始,所以得减一,第12个字符不匹配,也就是next[11]=3
所以说 :next[i]=第(i+1)个字符前面 j 位字符组成的字串的前后缀重合字符串
接下来从next[0]开始解释next数组
当i=0时,规定next[0]=-1。解释如下:
对于任何模式串,当模式串第一个字符就不匹配时,把模式串往后移动一个字符再做比较,就相当于模式串回退到它的第 -1 个字符串再进行比较。如图:
接下来以a b a a b c a c字符串做例子计算模式串的next数组
当i=0时,没有重复字串,next[0]=-1 (第一个字符就不匹配)
当i=1时,i 前的字串为"a" , next[1]=0 (第二个字符不匹配)
当i=2时,i 前的字串为"ab" , next[2]=0 (第三个字符不匹配)
当i=3时,i 前的字串为"aba" , next[1]=1 (第四个字符不匹配)
当i=4时,i 前的字串为"abaa" , next[2]=1 (第五个字符不匹配)
当i=5时,i 前的字串为"abaab" , next[2]=2(第六个字符不匹配)
当j=6时,i 前的字串为"abaabc" , next[1]=0(第七个字符不匹配)
当j=7时,i 前的字串为"abaabca" , next[2]=1(第八个字符不匹配)
为了说明get_next函数的代码,我先说明一下准备代码:
首先创建一个包括字符串和字符串的长度的结构体:
typedef struct { char ch[maxsize]; //存储串的一维数组 int length; //串的当前长度 } sstring;
然后是创建字符串的函数代码,最后代码一起放的时候提一下:
void creat(sstring &s) { int i=0; s.length=0; scanf("%s",s.ch); while(s.ch[i]) { s.length++; i++; } }
接着就是get_next函数的代码(这个代码相当难理解,直到今天我灵光一闪,悟了它求最大公共前后缀的办法。先上代码,我再解释):
void getnext(sstring t,int *next) { int i=0,j=-1; next[0]=-1; while(i<t.length-1) { if(j==-1||t.ch[i]==t.ch[j]) { i++; j++; next[i]=j; } else j=next[j]; } }
求最大公共前后缀,最好的办法莫过于再拿一条和模式串一模一样的字符串和模式串来做比较。如图:
一次一次来解释:
第一次:
两条模式串完全相同,自然是要模式串 s2 后退一个开始和模式串 s1 比较。
i 表示第一条串 s1 正在比较的字符位置,j 表示第二条串 s2 正在比较的字符位置。
把 s1 看作主串,s2看作模式串,现在就是看 s1 能不能匹配 s2 的前缀字串。这就又回到了匹配算法。
现在模式串的a和主串的字符b不匹配。由next数组的定义得,模式串要回退到next[j] , 这就是以下代码中 j=next[j] 的由来
while(i<t.length-1) { if(j==-1||t.ch[i]==t.ch[j]) { i++; j++; next[i]=j; } else j=next[j]; }
next数组的定义:当主串与模式串的某一位字符不匹配时,模式串要回退的位置的数组下标。
那么当匹配到相同的字符时又怎么办呢
看第三次的图:
s1[3]==s2[0] , 字符’a’等于字符’a’,那么就 i++ , j++ 再往后看一个字符看是不是也相等,如果相等,那么最大公共前后缀就又+1,而 j 在比较完相等后就加了一。
就比如上图,s2[1]==s1[4],所以i++,j++,即此时j=2
所以 j 就表示着最大公共前后缀的值。
所以这就是以下代码的由来
if(j==-1||t.ch[i]==t.ch[j]) { i++; //i加了一,指向匹配成功的字符的后一个字符 j++; next[i]=j; //当第i个字符匹配失败时,模式串要回退的位置 }
因为两条字符串一模一样,所以第二条字符串的所有字符都用第一条字符串的字符 t.ch[j] 来表示。
总结一下,就是模式串自己和自己匹配,匹配失败的字符就第二条模式串回退,匹配成功就往后看下一个字符是不是也匹配成功。因为两条模式串一模一样,所以字符用一个字符串数组来表示就行。
next数组介绍完就可以解释kmp函数了。虽然它是最核心的代码,但是我认为get_next函数才是最重要的,因为在get_next函数中就用到了kmp的思想。
其实kmp的关键思想就是next数组吧。所以接下来的kmp函数代码,看起来应该会轻松很多了。
int kmp(sstring s,sstring t,int pos) { int i,j; i=pos-1; //pos表示模式串和主串的第pos个字符开始匹配 j=0; while(i<s.length&&j<t.length) { if(j==-1||s.ch[i]==t.ch[j]) { //j=-1或字符匹配成功,就往下接着匹配 i++; j++; } else j=next[j]; //字符匹配失败,模式串回退 } if(j==t.length) return i-j+1; //返回主串中匹配成功的末尾字符下标 else return 0; }
接下来是kmp的全部代码,亲测可行:
#include<stdio.h> #include<stdlib.h> #define maxsize 250 int next[maxsize]; using namespace std; typedef struct { char ch[maxsize]; //存储串的一维数组 int length; //串的当前长度 } sstring; void creat(sstring &s) { int i=0; s.length=0; scanf("%s",s.ch); while(s.ch[i]) { s.length++; i++; } } void getnext(sstring t,int *next) { int i=0,j=-1; next[0]=-1; while(i<t.length-1) { if(j==-1||t.ch[i]==t.ch[j]) { i++; j++; next[i]=j; } else j=next[j]; } } int kmp(sstring s,sstring t,int pos) { int i,j; i=pos-1; j=0; while(i<s.length&&j<t.length) { if(j==-1||s.ch[i]==t.ch[j]) { i++; j++; } else j=next[j]; } if(j==t.length) return i-j+1; else return 0; } void getnextval(sstring t,int nextval[]) { int i,j; i=0;nextval[0]=-1;j=-1; while(i<t.length) { if(j==-1||t.ch[i]==t.ch[j]) { i++;j++; if(t.ch[i]!=t.ch[j])nextval[i]=j; else nextval[i]=nextval[j]; } else j=nextval[j]; } } int main() { int m; sstring s,t; creat(s); creat(t); // getnextval(t,next); getnext(t,next); m=kmp(s,t,1); printf("%d",m); }
其中getnextval(sstring t,int nextval[])函数是get_next(sstring t,int *next)函数的升级和优化,我懒得写了,下次吧 orz