前言:看毛片算法漫画讲解超幽默有爱~
一,KMP算法解决什么类型的问题
String str1 = "bacbababadababacambabacaddababacasdsd";
String str2 = "ababaca";
由以上字符串我们知道,str1有两处包含str2
分别在str1的下标为10,26的位置
“bacbababad**ababaca**mbabacadd**ababaca**sdsd”;
二,算法讲解与说明
一般匹配字符串时,我们从目标字符串str1(假设长度为n)的第一个下标选取和str2长度(长度为m)一样的子字符串进行比较,直到str1的末尾(实际比较时,下标移动到n-m)。这样子的复杂度就是O(n*m)。而KMP算法的复杂度可以优化为O(n+m)
简化时间复杂度的原因:充分利用了目标字符串str2的性质(比如里面部分的字符串的重复性,即使不存在重复字段,在比较时实现最大的移动量。
三,考察目标字符串str2
ababaca
这里我们先要计算一个长度为m的转移函数next。next数组的含义就是一个固定字符串的最长前缀和最长后缀相同的长度。
比如:abcjkdabc,那么这个数组的最长前缀和最长后缀相同的话必然是abc。
cbcbc,这个数组的最长前缀以及最长后缀相同就是cbc。
abcbc,最长前缀以及最长后缀相同就是不存在的。
注意:最长前缀是说以第一个字符开始,但是不包含最后一个字符。
比如我们说aaaa的最长前缀以及后缀都是aaa。
对于目标字符串str2,ababaca,长度为7,next[0],next[1],next[2],next[3],next[4],next[5],next[6]分别计算的是a,aab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀的长度。
由于a,ab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀是“”,“”,“a”,“ab”,“aba”,“”,“a”, 所以next数组的值是[-1,-1,0,1,2,-1,0]。这里-1表示不存在,0表示存在长度为1,2表示长度为3.这是为了和代码相对应。
下图中的1,2,3,4是一样的。1-2之间的和3-4之间的也是一样的,我们发现A和B不一样;之前的算法是我把下面的字符串向前移动了一个距离。重新重头开始比较,那必然存在很多重复的比较。现在的做法就是把下面的字符串往前移动,使得3和2对其,直接比较C和A是否一样。
四,代码解析继续
假设现在文本串是匹配到了i位置,模式串匹配到了j位置
(1)如果j = -1, 或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
继续匹配下一个字符。
(2)如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则i不变,
而j = next[j] .此举意味着失配时,模式串P相对于文本串S向右移动了
j - next[j] 位。
换句话说就是当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next数组,next数组 的求解后续会重点讲解,即也就是移动的实际位数为:j - next[j]。且此值大于等于1。
(3)很快你就会发现next数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next[j] = k,代表j指针之前的匹配上的字符串中有最大长度为k的相同前缀后缀。
(4)其实这也意味着在某个字符串失配时,该字符对应的next值会告诉你下一步匹配中。模式串应该跳到哪个位置(跳到next[j]位置)。如果next[j] 等于0
或者-1,则跳到模式串的开头字符,若next[j] = k或者k>0,代表下次匹配跳到j之前的某个字符,而不是跳到开头,且具体跳过了K个字符。
四,KMP算法代码里面最关键代码参数说明
1. next[j] = k
/**
* 获取执行KMP算法需要的next数组(采用递归的方式获取)
*/
public static void getnext(String ptr,int[] next){
char[] p = ptr.toCharArray();
int plen = ptr.length();
next[0] = -1;
int k = -1;
int j = 0;
while(j < plen - 1){
// p[k]表示前缀,p[j]表示后缀
if(k == -1 || p[j] == p[k]){
++ k;
++ j;
if(p[j] != p[k]){
next[j] = k;
}else {
// 因为不能出现p[j] == p[next[j]],所以出现之后继续递归,k = next[k] =next[next[k]]
next[j] = next[k];
}
}else{
k = next[k];
}
}
}
其中最关键的就是next[j] = k,就是由以上说明得来的。
2, k = next[k];
这个式子得来的关键就是使用模式串的自我匹配方式寻找到公共的前后缀。
// 因为不能出现p[j] == p[next[j]],所以出现之后继续递归,k = next[k] =next[next[k]]
next[j] = next[k];
这个就是防止重复出现上一次的失配情况。
五,最后是KMP算法解决字符串匹配位置问题的源代码
/**
* 使用KMP算法实现在str字符串中查找ptr字符串称为模式字符串
*/
import java.util.Scanner;
public class KMP {
public static void main(String[] args){
Scanner cin = new Scanner(System.in);
String str = "abcderde";
String ptr = "bcd";
int[] next = new int[ptr.length()];
getnext(ptr,next);
int ans = kmpsearch(str,ptr,next);
System.out.println(ans);;
}
/**
* 获取字符串开始匹配的位置
*/
public static int kmpsearch(String str,String ptr,int[] next){
char[] s = str.toCharArray();
char[] p = ptr.toCharArray();
int i = 0;
int j = 0;
int slen = str.length();
int plen = ptr.length();
while(i < slen && j < plen){
// 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),我们都是令i++,j++.
if(j == -1 || s[i] == p[j]){
i++;
j++;
}else{
j = next[j];
}
}
if(j == plen){
return i - j;
}else{
return -1;
}
}
/**
* 获取执行KMP算法需要的next数组(采用递归的方式获取)
*/
public static void getnext(String ptr,int[] next){
char[] p = ptr.toCharArray();
int plen = ptr.length();
next[0] = -1;
int k = -1;
int j = 0;
while(j < plen - 1){
// p[k]表示前缀,p[j]表示后缀
if(k == -1 || p[j] == p[k]){
++ k;
++ j;
if(p[j] != p[k]){
next[j] = k;
}else {
// 因为不能出现p[j] == p[next[j]],所以出现之后继续递归,k = next[k] =next[next[k]]
next[j] = next[k];
}
}else{
k = next[k];
}
}
}
}
结尾:给大家一个大神的链接,超详细的讲解KMP算法,基本上没有任何的理解问题。
非常详细而且还带有源代码解析,感谢博主的分享~