一、KMP算法的作用
减少常规暴力算法中不必要的回溯。
二、明确什么是字符串的前缀、后缀
以下面字符串str举例
a a b d a a b
前缀 | 后缀 |
---|---|
a | b |
aa | ab |
aab | aab |
aabd | daab |
aabda | bdaab |
aabdaa | abdaab |
aabdaab | aabdaab |
三、减少回溯的方式
通过使用next数组,使得被模板字符串和主串失配的地方可以快速回溯到上一处具有最长公共前缀后缀长度的地方,同时避免回溯到主串的源头。以此减少时间复杂度。
四、如何生成next数组
首先给出模板字符串m:abaabbabaab
主串s:abaabaabbabaaabaabbabaab
定义一个数组next
int next[] = new int [s.length];
下面需要明确一个定义,即什么叫做最大公共前缀后缀长度。
下面我们就拿m串进行举例,我们从m串为a、ab、aba,逐渐增加到abaabbabaab来观察和理解最大公共前缀后缀长度的含义。
红色字体标记为当前的最长公共前缀后缀
next数组
next[0]=-1 | m串 | 最大公共前缀后缀长度 |
---|---|---|
next[1]=0 | a | 0 |
next[2]=0 | ab | 0 |
next[3]=1 | aba | a (1) |
next[4]=1 | abaa | a (1) |
next[5]=2 | abaab | ab (2) |
next[6]=0 | abaabb | 0 |
next[7]=1 | abaabba | a (1) |
next[8]=2 | abaabbab | ab (2) |
next[9]=3 | abaabbaba | aba (3) |
next[10]=4 | abaabbabaa | abaa (4) |
Next数组不可越界所以无next[11] | abaabbabaab | abaab (5) |
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
next | -1 | 0 | 0 | 1 | 1 | 2 | 0 | 1 | 2 | 3 | 4 |
在上表中可以看到,我们将m串的最大公共前缀后缀长度存储到了next数组中。那么他们的意义到底是什么呢?
意义为:如果模板串在与主串匹配过程中发生了失配情况,那么我只需要将模板串倒退一定位数就可以继续进行匹配,以此来避免回溯主串。而这个模板串回退到的位置就是next数组中存储的元素。
五、如何使用next数组
我们举例说明一下,假设我的模板串abaabbabaab在最后一个字符b的匹配过程中出现了失配,我们就可以利用next数组,将模板串回退到next[next[10]],即next[4]处abaabbabaab,然后再次与之前失配的地方再次进行匹配,如果成功了就继续匹配下一个模板串的字符,如果没有成功,就再次执行上述的回退操作。详见下图
六、伪代码实现KMP算法
1、生成next数组
cn:=0; next[0]:=-1; next[1]:=0;
while(j遍历到模板串尾或cn回溯到-1){
if(m[i-1]==m[cn]){
next[i]=++cn;
i++;
}else if(m[i-1]!=m[cn]){
cn=next[cn];//cn作为游标开始进行回溯
}考虑情况如果游标回溯到-1
if(。。。){
游标回到-1说明此过程中没有能够和当前后缀匹配的前缀。故需要更新最大长度
}
}
2、KMP匹配过程
建立遍历主串用的指针i、遍历模板串使用的指针j
while(i<主串长度且j小于模板串长度){
if(m[j]==s[i]){
说明当前字符匹配,继续匹配下一个
i++;
j++;
}else if(不相等){
利用上面获取的next数组,使j回溯。
同属需要考虑到如果j回溯到j=-1的情况时,应该跳过当前i重新开始对下一个i进行匹配。
}
七、java语言实现
import java.util.*;
public class KMPtest {
static String str;
static String m;
static int next[];
static char strs[];
static char ms[];
static int cn = 0;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
str = in.nextLine();
m = in.nextLine();
strs = str.toCharArray();
ms = m.toCharArray();
next = new int[m.length()];
int k = 2;
next[0] = -1;
next[1] = 0;
while (k < ms.length && cn != -1) {
if (ms[k - 1] == ms[cn]) {//用于建立next数组
next[k] = ++cn;
k++;
} else if (ms[k - 1] != ms[cn]) {
cn = next[cn];
}
if (cn == -1) {
next[k] = 0;
cn = 0;
k++;
}
}
int i = 0;// 用于遍历主串
int j = 0;// 用于遍历模板串
while (j < ms.length && i < strs.length) {//进行模板串和主串的匹配
if (strs[i] == ms[j]) {
j++;
i++;
} else if (strs[i] != ms[j]) {
if (next[j] == -1) {
i++;
} else {
j = next[j];
}
}
}
System.out.println("匹配位置为:" + (i - j + 1));
}
}