核心原理:
从主串中查找是否包含子串abcdabcy
a b c x a b c d a b x a b c d a b c d a b c y
|
a b c d a b c y
从头开始对比abc都通过,知道第四位d与主串中x不匹配。此时需要看d之前的子串中是否包含相同前后缀,此时abc中不存在相同前后缀。没有则需要回到开头继续比对,头a移动到x。(因为第一次对比前三位abc都一致所以已经排除从前三位开始匹配的可能)
a b c x a b c d a b x a b c d a b c d a b c y
|
a b c d a b c y
a与x不匹配则继续向后移动
a b c x a b c d a b x a b c d a b c d a b c y
|
a b c d a b c y
前面abcdab6个字符完全一致,直到第7位c时与母串x不匹配。此时需要判断c之前的abcdab中是否有相同的前后缀,找出abcdab中最长前后缀为:ab。因为x与c之前的子串是匹配的,所以x之前一定为ab。这也意味着在前缀与后缀相同时可以,下一次对比中,主串可以跳过相同前后缀的ab。直接让x和子串中的前缀后第一个字符c进行匹配。
a b c x a b c d a b x a b c d a b c d a b c y
|
a b c d a b c y
c与x不匹配,检查子串c之前被标记的字符串中是否含有相同的前后缀。此时abc中没有,所以下次需要回到头从a开始比较
a b c x a b c d a b x a b c d a b c d a b c y
|
a b c d a b c y
a b c x a b c d a b x a b c d a b c d a b c y
|
a b c d a b c y
a与x不匹配向后移动一位,此时前面abcdabc7个子字符串都相等最后d与y无法匹配。同理继续看y之前子字符串abcdabc中是否存在相同前后缀,找出最大前后缀存在为:abc。所以下次对比可以直接从前缀后的字符d与母串中不匹配的d位置对比。此时子串d与母串d相同继续往后对比,后续a b c y完全一致,且已经到达母串末尾不存在其他后续情况到此结束。
a b c x a b c d a b x a b c d a b c d a b c y
|
a b c d a b c y
从上述简单流程可以看出KMP算法的核心思想就是找出子串中前后缀相等的字符串,达到跳跃的目的。不再需要像BM暴力算法一样循规蹈矩的从前往后一个一个对比。
一个字符串
abcdabcd......
如果此时发现子串abcdabcy最后一位不相等,我们还有必要继续将子串中的a后移一位与b比较么?当然不必要,此时只有一种可能会达到匹配的情形,
就是子串头移动到后缀的地方
a b c d a b c d
|
a b c d a b c y
所以,在kmp算法中遇到有前后缀的情况进行直接跳跃,就省去了很大一部分时间。与BM暴力算法相比时间复杂度直接从O(mn)变为了O(m+n)
下面一起看看,一个标准的KMP算法需要经过怎么的流程:
标准模式:
子串临时数组就是统计出我们前面查找的所有相同前后缀的可能存在情况
子串临时数组构建:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
j | i | ||||||
a | b | c | d | a | b | c | a |
0 | b | c | d | a | b | c | a |
从下标0位置开始,变量j在0位置,变量i在1位置。若果i与j的值不同就在数列中写入0同时i向后移动一位
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
j | i | ||||||
a | b | c | d | a | b | c | a |
0 | 0 | c | d | a | b | c | a |
i和j上的字符仍然不同写0继续向后移动
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
j | i | ||||||
a | b | c | d | a | b | c | a |
0 | 0 | 0 | d | a | b | c | a |
a与d不等写0继续往后移动
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
j | i | ||||||
a | b | c | d | a | b | c | a |
0 | 0 | 0 | 0 | a | b | c | a |
此时j与i相等都是a,写入的数字为j+1。即此时a临时数组对应值为1,同时j向后移动一位j此时等于1。此处的1即代表着b之前有一个长度为1的字符串是子串的前后缀
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
j | i | ||||||
a | b | c | d | a | b | c | a |
0 | 0 | 0 | 0 | 1 | b | c | a |
同上b相等,写入的值为j+1=2。i与j同时往后移动一位
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
j | i | ||||||
a | b | c | d | a | b | c | a |
0 | 0 | 0 | 0 | 1 | 2 | c | a |
c相等写入临时表j+1=3
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
j | i | ||||||
a | b | c | d | a | b | c | a |
0 | 0 | 0 | 0 | 1 | 2 | 3 | a |
到这里d与a不相等了,此时需要找到d之前的字符所对应的值c对应0。将c的值0赋值给j。再次对比j与i的值此时相等都为a,写入j+1
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
j | i | ||||||
a | b | c | d | a | b | c | a |
0 | 0 | 0 | 0 | 1 | 2 | 3 | 1 |
这样临时数组就求出来了,如果将上述过程换成更直观的理解就是
a 0
ab 0
abc 0
abcd 0
abcda 1 此时最长相同前后缀为a,长度为1
abcdab 2 此时最长相同前后缀为ab,长度为2
abcdabc 3 此时最长相同前后缀为abc,长度为3
abcdabca 1 此时最长相同前后缀为a,长度为1
是不是瞬间清晰了呢,第一种表格对应的其实只是代码的演变过程。它们的最终目的都是一样的,下面我们看看这个临时数组在算法中是怎么用的,将起到什么作用。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
a | b | c | d | a | b | c | a |
0 | 0 | 0 | 0 | 1 | 2 | 3 | 1 |
假设此时需要寻找的主串为:cdaccabcdabcdabca
c d a c c a b c d a b c d a b c a
a b c d a b c a
a与c不匹配往后移动一位,a与d不匹配往后移动一位
c d a c c a b c d a b c d a b c a
|
a b c d a b c a
此时a匹配,第二位b与c不匹配。此时查临时数组b前一位对应值为0,所以下一轮匹配从子串下标0开始
c d a c c a b c d a b c d a b c a
|
a b c d a b c a
a与c不匹配往后移动一位,还是c仍然不匹配继续往后移动,这次过后a找到了匹配值
c d a c c a b c d a b c d a b c a
|
a b c d a b c a
对比abcdabc前7位都匹配可是最后一位,a与d不匹配。此时查表发现a前一位c对应值为3,所以下次匹配直接从子串下标3处开始对比
c d a c c a b c d a b c d a b c a
|
a b c d a b c a
到这里dabca都相等就找到了对应值,因为已经到末尾也不需要继续判断。到这里一个标准的流程就走完了,其实核心原理还是跟开头举的例子一样,只不过是把找对应相同前后缀的过程写入到了临时数组中。
就拿我们最后一步来看abcdabc这串子串都相同,它对应的最长相同前后缀为abc长度为3所以下次查找不需要再从头a开始对比,只需要从c后面的d对比即可
对应java代码实现:
public class KMP {
public static int kmp(String str, String dest,int[] next){
for(int i = 0, j = 0; i < str.length(); i++){
while(j > 0 && str.charAt(i) != dest.charAt(j)){
j = next[j - 1];
}
if(str.charAt(i) == dest.charAt(j)){
j++;
}
if(j == dest.length()){
return i-j+1;
}
}
return 0;
}
public static int[] kmpnext(String dest){
int[] next = new int[dest.length()];
next[0] = 0;
for(int i = 1,j = 0; i < dest.length(); i++){
while(j > 0 && dest.charAt(j) != dest.charAt(i)){
j = next[j - 1];
}
if(dest.charAt(i) == dest.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}
public static void main(String[] args){
String a = "abcdabca";
String b = "cdaccabcdabcdabca";
int[] next = kmpnext(a);
int res = kmp(b, a,next);
System.out.println(res);
for(int i = 0; i < next.length; i++){
System.out.println(next[i]);
}
System.out.println(next.length);
}
}
成功返回匹配的对应数组下标9