引子:
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
花了3小时终于理解kmp算法了,kmp算法的思路并不难,但是许多资料讲的又难以让人理解,所以我希望我的这篇文章能够帮助到后来人,如果我写的内容有差错,希望大牛指出,帮助我改进学习。
算法思路
*** 如果有两个字符串,char[] str1和char[] str2,我们的目的是要在A字符串里找到B字符串的位置。这里我对两个变量的设置如下:
- str1=“ABEABABCABE”
- str2=“ABCAB”
这里我们先用朴素算法来查找str2在str1的位置吧。
第一次:
str1: | A | B | E | A | B | A | B | C | A | B | E |
---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B |
第二次:
str1: | A | B | E | A | B | A | B | C | A | B | E |
---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B |
第三次:
str1: | A | B | E | A | B | A | B | C | A | B | E |
---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B |
。
。
。
直到找到:
str1: | A | B | E | A | B | A | B | C | A | B | E |
---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B |
得出str2在str1的位置为6。
实现代码
public class demo1 {
public static void main(String[] args) {
int p2=0;//指向p2的指针
char[] str1="ABEABABCABE".toCharArray();
char[] str2="ABCAB".toCharArray();
for(int i=0;i<str1.length;i++){
while(p2<str2.length){
if(str1[i+p2]!=str2[p2]){
p2=0;//两者不相等,str2指向的指针返回到起点处。
break;
}
p2++;
}
if(p2==str2.length){
System.out.println(i);//输出str2在st1的位置
break;
}
}
}
}
上述代码可以看出,这种算法的效率很低,依次去比对,浪费了许多时间,所以需要我们对算法进一步的改进,也就是所谓的kmp算法。
kmp算法思路
还是老规矩,我们手动模拟下过程
第一次:
str1: | A | B | E | A | B | A | B | C | A | B | E |
---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B |
我们发现第3格str1与str2的字节不相同,而前两格AB是两个不重复的字母,所以我们就得出第二次的操作了。
第二次:
str1: | A | B | E | A | B | A | B | C | A | B | E |
---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B |
第三次:
str1: | A | B | E | A | B | A | B | C | A | B | E |
---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B |
第四次
str1: | A | B | E | A | B | A | B | C | A | B | E |
---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B |
找到字符串,结束。。。。。。
怎样?我懂,你还是很疑惑,所以我又找了个其他列子,方便你来理解。
- str1=“ABACDABABC”
- str2=“ABCABCD”
第一次
str1: | A | B | C | A | B | A | B | C | A | B | C | D |
---|---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B | C | D |
我们发现第6格str1与str2的字节不相同,前面五格ABCAB,C前面的AB与C后面的AB是重复的(好像是废话。。。),所以我们就得出第二步这样的操作了。
第二次
str1: | A | B | C | A | B | A | B | C | A | B | C | D |
---|---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B | C | D |
再次比较,我们发现还是第6格不同,而str2中C前面的字母没有重复的,所以就得出我们第三步这样的操作啦
第三次
str1: | A | B | C | A | B | A | B | C | A | B | C | D |
---|---|---|---|---|---|---|---|---|---|---|---|---|
str2: | A | B | C | A | B | C | D |
这里,我们就遍历得到str2在str1的位置了,与朴素算法相比,大大减少了遍历的时间,因为str2并不是像朴素算法一样一步一步按部就班,而是不讲武德,可能走一步,也可能走多步,如果你能自己手动模拟出上面的操作,那么就可以看如下的文字。
重点
这里我先说明一下字符串的前缀和后缀,字符串的前缀和后缀不能包含他本身, 如字符串"AABAE"
前缀:A,AA,AAB,AABA。后缀:E,AE,BAE,ABAE。
- str1=“ABACDABABC”
- str2=“ABCABCD”
我们将字符串str2拆分下:A AB ABC ABCA ABCAB ABCABC ABCABCD(这里并不是表示前缀和后缀,不要误解),然后统计每个拆分好的字符串的前缀集合与后缀集合的交集中最长元素的长度。这里我以上述条件将拆分好的字符串列出如下一个表。
拆分的字符 | A | AB | ABC | ABCA | ABCAB | ABCABC | ABCABCD |
---|---|---|---|---|---|---|---|
Len | 0 | 0 | 0 | 1 | 2 | 3 | 0 |
这里我直接列公式
下次移动到的下标=str1与str2不同位置的下标-Len,Len是怎么决定的呢?如str1=“ABACDABABC” ,str2=“ABCABCD”
当ABACDABABC
ABCABCD
↑
我们发现C与A不同,而C前面的AB对应的Len为0,所以下一次移动的指向为2。
代码
说了这么多,我们回归主题吧,直接上代码吧!
public class demo1 {
public static void main(String[] args) {
int p2=0;//指向p2的指针
String dest="ABCDAB";
char[] str1="ABCABDEABABCDAB".toCharArray();
char[] str2=dest.toCharArray();
int [] Map=new int[str2.length];
int max;
//初始化那张表
for(int i=0;i<str2.length;i++){
max=0;
String text=dest.substring(0,i+1);//获取拆分的字符 A AB ABC ABCA ABCAB
for(int j=0;j<i;j++){
String s=text.substring(0,j+1);
String e=text.substring(text.length()-j-1,text.length());
if(e.equals(s)){
max=max>j+1?max:j+1;
}
}
Map[i]=max;
}
//初始化完毕
int index=0,p=0;
while(index<str1.length){
if(str1[index++]!=str2[p++]){
index--;
index=index-Map[p-1];
p=0;
if(str1[index]!=str2[p])index++;
}
if(p==str2.length){
System.out.println(index-p);
return;
}
}
}
}
总而言之,你也许还是感觉很懵逼,但是至少思路清晰了点,对吧,那么,再见。