给定一个长度为n的主串t[0……n-1],和一个长度为m的模板串p[0……m-1],m<<n,找p在t中出现的位置。
假设文本串为str1,模板串为str2,i、j分别为str1和str2的索引。如果使用暴力匹配的方法,那么若当前字符成功匹配时,只需要让i和j继续往右遍历,匹配下一个字符,若当前字符没有成功匹配,需要让i回溯让j置0,即str2串需要从头开始遍历。但是在失配字符之前,存在已经匹配过的部分,使用KMP算法借助这一信息,在每次回溯时通过next数组找到前面已经匹配过的位置。
以文本串str1=”BBC ABCDAB ABCDABCDABDE”和模板串str2=”ABCDABD”为例,首先用str1与str2的第1个字符匹配,发现不符合,将str2右移1位,再进行比较,重复这一过程,直到str1中有1个字符与str2的第1个字符匹配;然后比较下一个字符,直到遇到不匹配的字符。此时情况如下图所示(i=10,j=6):
字符D和空字符不匹配,D之前的”ABCDAB”已经匹配,通过查询str2的部分匹配表来移动str2,移动位数=已经匹配的字符数-失配符的前一位对应部分匹配值,即6-2=4,所以把str2右移4位:
然后再重复以上过程,一直到str2的最后一位,发现可以完全匹配,搜索完成:
可以发现,问题的难点在于怎么找到“部分匹配值”。部分匹配值是当前字符为结尾的串的前缀与后缀的最长的共有元素的长度,即下图所示, 然后我们基于这个部分匹配值表进行匹配。
public class KMPmatch {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("请输入文本串:");
String str1 = in.nextLine();
System.out.println("请输入模式串:");
String str2 = in.nextLine();
int[] next = getNext(str2);
System.out.println("部分匹配值表为:" + Arrays.toString(next));
int index = kmpMatch(str1, str2, next);
if (index == -1) {
System.out.println("没有匹配到模式串");
Return;
}
System.out.println("模式串在文本串中的第一次出现下标是" + index);
in.close();
}
public static int[] getNext(String str2) {
int[] matchVal = new int[str2.length()];
matchVal[0] = 0;
int i = 0;
int j = 0;
for (i = 1, j = 0; i < str2.length(); i++) {
while (j > 0 && str2.charAt(i) != str2.charAt(j)) {
j = matchVal[j - 1];
}
if (str2.charAt(i) == str2.charAt(j)) {
j++;
}
matchVal[i] = j;
}
return matchVal;
}
public static int kmpMatch(String str1, String str2, int[] next) {
int i = 0;
int j = 0;
for (i = 0, j = 0; i < str1.length(); i++) {
// 没有匹配到,按照部分匹配值跳跃匹配
while (j > 0 && str2.charAt(j) != str1.charAt(i)) {
j = next[j - 1];
}
if (str2.charAt(j) == str1.charAt(i)) {
j++;
}
if (j == str2.length()) {
int res = i - (j - 1);
return res;
}
}
return -1;
}
}
与暴力匹配对比发现,KMP的原理比较好理解:从头开始比对2个串的字符,匹配就向前继续比对,不匹配就让模式串移动一定位数(根据前面已经匹配过的子串来确定可移动的位数),重复这些步骤一直到搜索完成。
但是对于怎么实现比较难理解:一是怎么构建部分匹配值表或者说怎么构建next数组,二是怎么能够让模式串在失配的情况下移动最长的位数。
首先,按照课堂讲的,next数组是等于部分匹配表整体右移一位,然后初值置为0得到的,但是我这里是直接让next数组等于部分匹配表,因为next数组要表达的含义实际上就是模式串中相同前缀与后缀的最大长度。在实际应用中因为移动位数是依赖于前一个字符的串的长度,用不到当前失配字符的匹配值,所以第一种表示形式是把匹配值后移得到next。由于next是这样形成的,所以可以直接把部分匹配值当做是next数组。
那么怎么求next数组,其核心在于当某一对前后缀的最后一个字符不匹配时,寻找长度更短的前后缀进行比对,这个过程相当于模式串自己和自己进行匹配,于是当失配时,不断递归j=matchVal[j-1],直到找到了更短的相同前后缀或者根本就没有相同的前后缀。
然后关于怎么使用next来进行匹配,KMP的匹配流程是:先匹配第一个字符,符合的话,就重复匹配后续字符,不符合的话,就把模式串后移,然后重复前面的过程。直到再次遇到失配情况,这时,因为前面已经有了一部分字符是匹配过的,所以根据这个信息,让模式串直接跳过一定长度后再匹配。重复这个过程直到匹配结束。这里的移动位数是:
移动位数=已匹配字符数-失配符前一个字符的部分匹配值(即前面匹配串的最大公共长度)。根据这个就可以得出,当失配时,通过让j=next[j-1]来移动(j-next[j-1])位。
参考:b站韩顺平老师的Java数据结构课,和一篇写的很详细也很容易理解的博客:很详尽KMP算法(厉害) - ZzUuOo666 - 博客园