K(看)M(毛)P(片)算法最常用在字符串匹配。给定一个长的字符串(target string)和一个短的字符串(pattern string),要求判断pattern string是否是target string的子串,如果是,则返回子串的首个字符的下标;如果否,则返回-1。
解决这个问题最常想到的办法就是brutal force,即从target string第一个字符开始与pattern string比较,如果相等则比较target string和pattern string的下一个字符,如果不等则返回到target string中相等的字符的下一个字符。换句话说,假设我们用target和pattern分别表示两个字符串的指针,那么每一次比较不管两个string匹配到何种程度,只要不是完全匹配(即匹配完成),那么target永远只能增加1,这个算法的复杂度为O(mn).(m=strlen(target string),n=strlen(pattern string)).
以下是这个算法的C代码。
1 int strstr(char *target, char *pattern) 2 { 3 int i,j; 4 for(i=0;;i++) 5 { 6 for(j=0;;j++) 7 { 8 if(pattern[j]==0)return i; 9 if(target[i+j]==0)return -1; 10 if(target[i+j]!=pattern[j])break; 11 } 12 } 13 }
导致这个算法时间复杂的关键,在于我们每次只能将target指针加一,而不能充分利用之前已匹配部分的信息。一个很好的例子由http://kenby.iteye.com/blog/1025599 给出,事实上我们可以通过利用已匹配部分的信息,让每次比较失败后target跳过多个位置。
下面给出KMP的代码再给出一些解释。
1 int* overlay(char *str) 2 { 3 int *a,len=strlen(str),i,index; 4 a[0]=-1;//美好的约定 5 a=(int *)calloc(len,sizeof(int)); 6 for(i=1;i<len;i++) 7 { 8 index=a[i-1];//上一个 9 while(index>=0&&a[index+1]!=a[i]) 10 { 11 index=a[index]; 12 } 13 if(a[index+1]==a[i]) 14 { 15 a[i]=index+1; 16 } 17 else a[i]=-1; 18 } 19 return a; 20 } 21 22 int strstr(char *target,char *pattern) 23 { 24 int i,j,*a,len1=strlen(target),len2=strlen(pattern); 25 a=overlay(pattern); 26 for(i=0,j=0;i<len1&&j<len2;) 27 { 28 if(target[i]==pattern[j]) 29 { 30 i++; 31 j++; 32 } 33 else if(j==0)i++;//若第一个字符就不相等,则对target的指针加1即可 34 else j=a[j-1]+1; 35 } 36 if(j==len2)return i-j; 37 else return -1; 38 }
overlay函数是用递推求出pattern串每一个位置对应的覆盖值,算法在http://blog.csdn.net/power721/article/details/6132380有解释,不赘述。覆盖值数组的含义在上述博客中没有解释得很清楚,我举个例子。
首先假设这个字符串名为str。首字符的覆盖值是-1,这是约定。从第二个字符开始,我们看到b的覆盖值是-1,什么意思呢,意思就是在b之前的子串(包括b),不存在k满足str[0]str[1]...str[k-1]str[k]=str[1-k]str[2-k]...str[1]。接下来,第三个字符a的覆盖值为0,表示在a之前的子串(包括a),存在k=0使得str[0]str[1]...str[k-1]str[k]=str[2-k]str[3-k]...str[2]。第五个字符b的覆盖值为1,表示在b之前的子串(包括b),存在k=1使得str[0]str[1]...str[k-1]str[k]=str[4-k]str[5-k]...str[4]。
至于overlay函数的递推求法,博客里有说明递推过程,我也是看了好一会才看清楚orz...下面配个图,图里中括号里面是相同的字符串,小括号里面也是相等的字符串,以此第三次,第四次一直往下找...大家才思敏捷,一定可以看得懂。
其余的东西在以上两篇博客都讲得很清楚,我也不再赘述。算法导论里有关于KMP算法摊还分析的一些证明,在不同的地方,上文所述的覆盖值的含义有可能有一点点差别,命名也有不同,但总的意思和算法都是差不多的。
部分代码参考了https://leetcode.com/problems/implement-strstr/?tab=Description