题目是leetcode上的Implement strStr(),题目大意就是在字符串haystack中寻找字符串needle的是否存在,并返回位置。
地址是:
https://leetcode.com/problems/implement-strstr/,可以用来训练一下KMP算法的实现。
学习KMP算法的过程中,参考了这篇文章:
http://blog.csdn.net/yutianzuijin/article/details/11954939/
KMP算法的核心原理在于:传统的字符串匹配,一旦发生失配,那么模式字符串直接回退到最开始。但是在KMP算法中,我们根据模式字符串的特征,在失配的时候,根据已有信息尽可能少的回退。
这个信息就是:模式字符串的最长公共前后缀。比如说一个长度为4的字符串abab,那么最长公共前后缀就是ab,从左边数有个ab,右边也有个ab,长度为2。一旦匹配到最右边的b后发生失配,比如模式:ababc,文本:ababd,因为模式长度为4的部分左右对称,所以不需要全部回退,回退到左边的ab即可。
这个信息也就是存在最长公共前后缀中,我们用一个next数组来表示。比如next[4] = 2,就是长度为2的字符串最长前后缀值为2,于是回退到2即可,也就是ab后的下一个字符。
next数组计算方法:引用参考文章中的说明:
next数组计算
理解了kmp算法的基本原理,下一步就是要获得字符串f每一个位置的最大公共长度。这个最大公共长度在算法导论里面被记为next数组。在这里要注意一点,next数组表示的是长度,下标从1开始;但是在遍历原字符串时,下标还是从0开始。假设我们现在已经求得next[1]、next[2]、……next[i],分别表示长度为1到i的字符串的前缀和后缀最大公共长度,现在要求next[i+1]。由上图我们可以看到,如果位置i和位置next[i]处的两个字符相同(下标从零开始),则next[i+1]等于next[i]加1。如果两个位置的字符不相同,我们可以将长度为next[i]的字符串继续分割,获得其最大公共长度next[next[i]],然后再和位置i的字符比较。这是因为长度为next[i]前缀和后缀都可以分割成上部的构造,如果位置next[next[i]]和位置i的字符相同,则next[i+1]就等于next[next[i]]加1。如果不相等,就可以继续分割长度为next[next[i]]的字符串,直到字符串长度为0为止。由此我们可以写出求next数组的代码(Java版):
public int[] getNext(String b)
{
int len=b.length();
int j=0;
int next[]=new int[len+1];//next表示长度为i的字符串前缀和后缀的最长公共部分,从1开始
next[0]=next[1]=0;
for(int i=1;i<len;i++)//i表示字符串的下标,从0开始
{
//j在每次循环开始都表示next[i]的值,同时也表示需要比较的下一个位置。 这句注释一定要结合上面的说明理解,理解了KMP算法基本也就搞定了。
while(j>0&&b.charAt(i)!=b.charAt(j))j=next[j];
if(b.charAt(i)==b.charAt(j))j++;
next[i+1]=j;
}
return next;
}
这份代码中比较难理解的就是while循环,再举一个例子,比如模式字符串:
ababeababc,当我们计算了前九个字符,有next[9] = 4,也就是前9字符最长公共缀为abab。(注意next[9]表明字符串长度,而4是索引,也就是第五个字符,这块有点绕要注意理解)那么现在开始计算next[10],可以看到 j = next[9] = 4,string[4] != string[9],看这个时候next[4]是什么,是左边abab字符串的最长公共缀ab!由于abab中左右ab是对称的,这个时候最左边的ab加上一个a,和最右边的ab加上string[9]也就是c,有可能组成新的更长的公共前缀(当然本例中a!=c,所以不存在,继续拆分)。这就是计算next的精髓,通过j=next[j]加速寻找。因为要的到更长的最长公共缀,那么i之前和j之前的字符串必须是对应的。再举个例子 dabdabcdabdabd
d a b d a b c d a b d a b d
j i ,i和j处字符不同,回退
j i ,回退到next[j],字符相同
d a b d d a b d ,生成公共缀next[i+1]=j。
可以看到,左边和右边除了i,j位置外用到的字符串,实际上是
d a b d a b 的最长公共缀 d a b,如果没有回退到next[j],即使i和j字符相同,剩余的部分也无法匹配。
有点类似于一个递归的过程,只不过在递归过程中,我们不断移动j指针,使它指向可能存在结果的最长公共前缀的下一个字符,并和当前处理到的字符对比,来计算出新的next值
本题解:
public class Solution {
public int strStr(String haystack, String needle) {
if(needle.length() == 0) return 0;
int[] next = getNext(needle);
int i = 0, j = 0;
while(i<haystack.length()&& j < needle.length()){
if(haystack.charAt(i) == needle.charAt(j)){
i++;
j++;
}else{
if(j != 0){
j = next[j];
}else {
i++;
}
}
}
if(j == needle.length()) return i - needle.length();
else return -1;
}
public int[] getNext(String b)
{
int len=b.length();
int j=0;
int next[]=new int[len+1];
next[0]=next[1]=0;
for(int i=1;i<len;i++)
{
while(j>0&&b.charAt(i)!=b.charAt(j))j=next[j];
if(b.charAt(i)==b.charAt(j))j++;
next[i+1]=j;
}
return next;
}
}