蛮力搜索O(m*n)的时间复杂度,重复匹配步骤太多。KMP算法的核心是发现模式串中的“前缀=后缀”,避免在模式串j位置发生匹配失败时还要回退到0位置开始匹配。 发现“前缀=后缀”,只需要将模式串向前移动合适的位置,跳过了重复匹配前缀的步骤。
所以,KMP算法使用了一个next数组来保存模式串中的前缀=后缀的最大的k值,next[j]表示模式串的子串P0...Pj-1中使得P0...Pk-1 = Pj-k...Pj-1即前缀=后缀的最大k。构造next数组是KMP算法的核心工作。至于匹配的过程,主串依次扫描来匹配模式串,当模式串中的j位置匹配失败时,就让j=next[j]接着匹配,每次匹配失败后跳过合适的位置,也就是避免了再重复匹配前缀。
详细的分析可以阅读 http://www.cnblogs.com/huangxincheng/archive/2012/12/01/2796993.html
http://www.cnblogs.com/goagent/archive/2013/05/16/3068442.html
在下面这个例子中,第一轮当主串扫描到i位置与模式串的j位置没有匹配成功,此时模式串的子串aba存在“前缀=后缀”的情况也就是a=a,所以下一轮前缀a已经不用再比较了,直接去比较了b:
next[j]来记录失配时模式串应该用哪一个字符于Si进行比较。
设 next[j]=k。根据公式我们有
-1 当j=0时
next[j] = max{k| 0<k<j 且 P0P1..Pk-1=Pj-kPj-k+1...Pj-1}
0 其他情况
好,接下来的问题就是如何求出next[j],这个也就是kmp思想的核心,对于next[j]的求法,我们采用递推法,现在我们知道了next[j]=k,我们来求next[j+1]=?的问题?其实也就是两种情况:
①:Pk=Pj 时 则P0P1...Pk=Pj-kPj-k+1...Pj, 则我们知:
next[j+1]=k+1。
又因为next[j]=k,则
next[j+1]=next[j]+1。
②:Pk!=Pj 时 则P0P1...Pk!=Pj-kPj-k+1...Pj,这种情况我们有点蛋疼,其实这里我们又将模式串的匹配问题转化为了上面我们提到的”主串“和”模式串“中寻找next的问题,你可以理解成在模式串的前缀串和后缀串中寻找next[j]的问题。现在我们的思路就是一定要找到这个k2,使得Pk2=Pj,然后将k2代入①就可以了。
设 k2=next[k]。 则有P0P1...Pk2-1=Pj-k2Pj-k2+1...Pj-1。
若 Pj=Pk2, 则 next[j+1]=k2+1=next[k]+1。
若 Pj!=Pk2, 则可以继续像上面递归的使用next,直到不存在k2为止。
Java代码实现如下:
/**
* 在主串中匹配模式串,并返回匹配到模式串的起始位置
* 蛮力搜索O(m*n)的时间复杂度,重复匹配步骤太多
* KMP算法的核心是发现模式串中的“前缀=后缀”,避免在模式串j位置发生匹配失败时还要回退到0位置开始匹配
* 发现“前缀=后缀”,只需要将模式串向前移动合适的位置,跳过了重复匹配前缀的步骤
* @author pixel
*
*/
public class KMP {
public static int kmp(String majorString, String pattern)
{
if (majorString == null || pattern == null || majorString == "" || pattern == "")
return 0;
int[] next = makeNext(pattern);
int i = 0, j = 0;
//只扫一遍主串并且不回退,每次当匹配模式串中的pattern[j]失败时,就将模式串前移next[j],并且避免了重复匹配
while (i < majorString.length() && j < pattern.length())
{
if (j == -1 || majorString.charAt(i) == pattern.charAt(j))
{
i ++;
j ++;
}
else
{
j = next[j];
}
}
if (j == pattern.length())
return i - j;
return -1;
}
/**
* 构造next数组,next[j]表示:
* 为了寻找在pattern串的子串P0...Pj中,
* 前缀P0 P1 ... Pk-1 = 后缀Pj-k Pj-k+1 ... Pj时,最大的k值
* @param pattern
* @return next数组
*/
private static int[] makeNext(String pattern)
{
int[] next = new int[pattern.length()];
int k = -1;
int j = 0;
//当j=0时next[j]为-1
next[j] = -1;
//递推next[j+1]
//next[j]是在P0...Pj-1中找前缀=后缀的最大k使得P0...Pk-1 = Pj-k...Pj-1
//所以next[j+1]就是在P0...Pj中找,而next[j]时已经判断了P[k-1]=P[j-1],现在只需判断P[k]?P[j]
while (j < pattern.length() - 1)
{
//Pk == Pj的情况:next[j+1]=k+1 也就是 next[j+1]=next[k]+1
if (k == -1 || pattern.charAt(k) == pattern.charAt(j))
{
next[++j] = ++k;
}
else
{
//Pk != Pj的情况:递推k = next[k],不断在上一次的前缀中寻找Pk=Pj
k = next[k];
}
}
return next;
}
public static void main(String args[])
{
String major = "aababcdad";
String pattern = "abcd";
System.out.println(kmp(major, pattern));
}
}