KMP算法介绍
-
KMP名字的来源,因为该算法是由D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。
-
该算法主要用于字符串匹配问题
-
假设主串长度n,模式串(主串相匹配的字符串)长度为m,如果使用暴力匹配算法,由于指针回溯的问题,最差的情况下的时间复杂度为
O(m*n)
-
KMP算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了
主串指针的回溯
,从而使算法效率有了某种程度的提高。
字符串最长公共前缀后缀
在说公共前后缀之前先说一下前缀和后缀:
- 前缀:指除了最后一个字符以外,一个字符串的全部头部
组合
- 后缀:指除了第一个字符以外,一个字符串的全部尾部
组合
光说概念有点晦涩,举个例子:假设字符串为ABCDAB
- 前缀组合为:[A, AB, ABC, ABCD, ABCDA]
- 后置组合为:[BCDAB, CDAB, DAB, AB, B]
- 所谓的公共前后缀,就是指后缀和前缀
相同
,从上面的两个组合中可以发现,相同的只有是AB,所AB即是公共前后缀,也是最大公共前后缀。
next数组
假设一个字符串为:ABAABCA
- 找出该字符串的
各个子串
的公共前后缀
字符串子串 | 前缀 | 后缀 | 最长公共前后缀 |
---|---|---|---|
A | 无 | 无 | 0 |
AB | A | B | 0 |
ABA | A ,AB | BA,A | 1 |
ABAA | A ,AB,ABA | BAA,AA,A | 1 |
ABAAB | A,AB ,ABA.ABAA | BAAB,AAB,AB ,B | 2 |
ABAABC | A,AB,ABA,ABAA,ABAAB | BAABC,AABC,ABC,BC,C | 0 |
ABAABCA | A ,AB,ABA,ABAA,ABAAB,ABAABC | BAABCA,AABCA,ABCA,BCA,CA,A | 1 |
- 这样,最长公共前后缀就会和串的每个字符产生一种对应关系:
子串 | A | B | A | A | B | C | A |
---|---|---|---|---|---|---|---|
公共前后缀长度 | 0 | 0 | 1 | 1 | 2 | 0 | 1 |
这个表的含义是:当前字符作为最后一个字符时,当前子串所拥有的公共前后缀最长长度
接下来我们就用这个表来引出next数组,next 数组的值是除当前字符外
(注意不包括当前字符)子串的公共前后缀最长长度,相当于把上表做一个变形,将表中公共前后缀最长长度全部右移一位,第一个值赋为-1。
字符 | A | B | A | A | B | C | A |
---|---|---|---|---|---|---|---|
next | -1 | 0 | 0 | 1 | 1 | 2 | 0 |
上面是我们手推导的,我们如何使用代码
求取这个next数组那,先分析一下思路:
-
1,next[0]=-1,我们用不到这个值,因为不包含A的子串就是空串,更不用说公共前后缀,所以把它设为-1,其他值也可以。
-
2,next[1]=0,单个字符没有所谓的前缀和后缀。
接下来才是我们需要求解的:i从0开始,j从1开始,i小于j;
- 3,求解next[2]的值
i=0;j=1;
T[i]!=T[j];
next[2]=0;
二者不等表明:每一个子串的
最大
公共前后缀不可能再相等了,从上面那个表中我们也能看出来,所以比较次大前缀和后缀,然后将j加1,i的指针回退:i=0
。
- 4,求解next[3]的值
i=0;j=2;
T[i]=T[j];
next[3]=1;
此时,表明
次大
的前缀后缀是可以相等的,因此将二者同时加1
;
- 5,求解next[4]的值
i=1;j=3;
T[i]!=T[j];
此时表明:
次大
的前后缀相等的情况到此为止,从这个位置向后位置的次大前后缀都不可能相等了,此时需要考虑三级前后缀是否相等了,指针i回退,i=0,j=3,我们先这样回退,等到后面在分析,其实这样回退并不太好。
i=0;j=3;
T[i]=T[j];
next[4]=1;
注意:三级公共前后缀相等,以后的情况就
不需要判断一级和二级公共前后缀了
,因此同时将二者加1
- 6,求解next[5]的值
i=1,j=4
T[i]=T[j];
next[5]=2;
以后的三级公共前后缀还可能相等,因此将两者都加1
- 7,求解next[6]的值
i=2,j=5
T[i]!=T[j];
从这位置开始,三级公共前后缀相等的情况也不存在了,指针回退,i=0,j=4,直接判断四级公共前后缀
i=0,j=4
T[i]!=T[j];
从这位置开始,四级公共前后缀相等的情况也不存在了,指针回退,i=0,j=5,直接判断五级公共前后缀
i=0,j=5
T[i]!=T[j];
next[6]=0;
通过上面的分析,虽然公共前后缀能求出来,但是用程序实现出来肯定是很麻烦的,因为一旦出现不匹配的情况,指针i和j都在回退
,代码也很难实现,我们可以利用前面已经匹配的结果,这样就避免重复的比较
,我们再来分析一下:
next数组实现代码:
public static int[] GetNext(String str) {
//将字符串转换为字符数组
char [] chars = str.toCharArray();
//定义next数组,长度和传入的模式串一样
int[] next=new int[str.length()];
//定义两个索引变量
int j=0,i=-1;
//next[0]=-1
next[j]=i;
//因为每次循环里面执行j++,然后在赋值,索引不减一会索引越界
//第一轮循环:给next[1]=0,从第二轮循环才真正有意义
while(j<str.length()-1){
if(i==-1||chars[j]==chars[i]){
j++;
i++;
next[j]=i;//用于记录匹配的最大公共前后缀的值
} else{
i=next[i];//匹配不成功回溯,使用之前已经匹配的结果
}
}
return next;
}
KMP算法代码实现
前面大费周章的说next数组,现在就要派上用场了,我们在做字符串匹配的时候,先求出模式串对应的next数组,然后使用模式串匹配主串的时候,回溯就不用从头回溯,而是使用next数组中的值进行回溯,原因和分析next数组是一样的,因为匹配成功的串必然是相等的,所以它们拥有相同的公共前后缀,回溯自然可以利用前面已经匹配的结果
,这也是KMP算法和BF(暴力匹配)的区别:
- 注意:如果第一个字符串就没有匹配成功,我们就采用和BF相同的处理方式:加一
public static int KMP(String mainStr,String patternStr){
//求出next数组
int[] next =GetNext(patternStr);
//将字符串转换为字符数组
char [] mainChar = mainStr.toCharArray();
char[] patternChar=patternStr.toCharArray();
//定义两个索引变量,i代表主串,j代表模式串
int i=0,j=0;
//退出循环的条件为,索引越界,i单独越界匹配失败,j单独越界表示匹配成功,同时越界表示匹配成功
while (i<mainChar.length && j<patternChar.length) {
//匹配成功:也就是mainChar[i]==patternChar[j],索引向后加1
//如果索引回溯到j=next[0]=-1:索引直接加1,也就是采用和BF相同的方法直接后移
if (j==-1 || mainChar[i]==patternChar[j]) {
//i,j各增1
i++;
j++;
} else {
//从串的索引回溯,主串索引不回溯,这个和求取next数组的原理是一样的
// 避免双回溯,使用之前比较过的结果
//注意:如果第一个字符就匹配失败,就会出现j=next[0]=-1的情况
j=next[j];
}
}
// 判断是不是匹配成功,也就是索引j越界
if (j>=patternChar.length)
return(i-j); //返回匹配模式串的首字符下标
else
return(-1); //返回不匹配标志
}
完整代码
public class Test01 {
public static void main(String[] args) {
String mainStr="ABAABABAABAABCA";
String patternStr="ABAABCA";
int num=KMP(mainStr,patternStr);
for(int i=0;i<patternStr.length();i++){
System.out.print(mainStr.charAt(num+i));
}
}
public static int[] GetNext(String str) {
int[] next=new int[str.length()];
//定义两个索引变量
int j=0,i=-1;
//next[0]=-1
next[j]=i;
//因为每次循环里面执行j++,然后在赋值,索引不减一会索引越界
//第一轮循环:给next[1]=0,从第二轮循环才真正有意义
while(j<str.length()-1){
if(i==-1||str.charAt(j)==str.charAt(i)){
j++;
i++;
next[j]=i;//用于记录匹配的最大公共前后缀的值
} else{
i=next[i];//匹配不成功回溯,使用之前已经匹配的结果
}
}
return next;
}
public static int KMP(String mainStr,String patternStr){
//求出next数组
int[] next =GetNext(patternStr);
//定义两个索引变量,i代表主串,j代表模式串
int i=0,j=0;
//退出循环的条件为,索引越界,i单独越界匹配失败,j单独越界表示匹配成功,同时越界表示匹配成功
while (i<mainStr.length() && j<patternStr.length()) {
//匹配成功:也就是mainChar[i]==patternChar[j],索引向后加1
//如果索引回溯到j=next[0]=-1:索引直接加1,也就是采用和BF相同的方法直接后移
if (j==-1 || mainStr.charAt(i)==patternStr.charAt(j)) {
//i,j各增1
i++;
j++;
} else {
//从串的索引回溯,主串索引不回溯,这个和求取next数组的原理是一样的
// 避免双回溯,使用之前比较过的结果
//注意:如果第一个字符就匹配失败,就会出现j=next[0]=-1的情况
j=next[j];
}
}
// 判断是不是匹配成功,也就是索引j越界
if (j>=patternStr.length())
return(i-j); //返回匹配模式串的首字符下标
else
return(-1); //返回不匹配标志
}
}
拓展
其实KMP算法还可以继续优化,由于时间和精力有限,就不做探讨了,,,,,
有精力的小伙伴可以参考这篇博客:传送门