KMP算法(文本串S,模式串P)
建议:先记住这里个串,以免后面不知道哪个大写字符代表什么串。
对于模式串匹配的问题,之前我们都是采用暴力匹配的思路,就是假设现在有个文本串S匹配到 i 位置,一个模式串P匹配到 j 位置,则有:
如果当前字符匹配成功(即S[i] == P[j]),则 i++,j++,继续匹配下一个字符;
如果当前字符匹配失配(即S[i]! = P[j]),则 i = i - j + 1(i进行回溯),j = 0,继续开始匹配,直到匹配完毕。
但是我们发现上面这个例子当 i 回溯过去后,S[5]肯定跟P[0]失配,因为在之前匹配中,我们已经得知S[5] = P[1] = B,而P[0] = A,即P[1] != P[0],故S[5]必定不等于P[0],所以回溯过去必然会导致失配。虽然我们已经知道结果肯定是失配的,但是这种算发无法改变这种情况,如果数据输入规模较大的话,对于这种匹配必然失配的情况就会增加额外的比较,比较这种基本运算是影响算法时间复杂度的。
优化之后可以有一种让i直接回溯到需要判断的位置,在思想上可以看成是移动模式串到有效的匹配位置之后与文本串进行匹配的算法,即 KMP算法。它利用之前已经部分匹配这个有效信息,使i在回溯时尽量到有效的位置(可以减少大量的比较次数,降低算法时间复杂度),那么这个有效信息就需要去获取,这时候就引入了next数组,这里先说一下next 数组各值的含义:代表当前字符(包括当前字符)之前的字符串中,有多大长度的相同前缀后缀。
我们来看一下next数组是如何分析来的:i代表next数组的角标,j代表模式串的子串最大角标也是当前next[i]的值。
这里我就不用动图展示了,原因是动图演示太快,你们有可能看不清,故你们可以将此图沾到画图板上对照着代码,自己走一下过程(完全想不出如何走的),不过如果你可以自己走出这些过程尽量不要看代码,可能会限制你的思维方式,我是尝试了好长时间才走出的过程,然后写出的代码。 第一个字符串是正常事例 第二个字符串是当j=2时,next[6]的本来是0,结果却是2的事例 如何解决呢?从极限角度去考虑,当有一次相等后,即j>0,下一次比较不相等时,可以这样做j = next[j - 1];有些人可能会担心当j特别大时,next[j - 1]的值不为0,其实这是不可能的,如果j特别大,说明连续长度大小的模式串的子串相同前缀后缀特别多,例如ABCDEFGHIJK ABCDEFGHIJK M这个模式串,j最终等于10,但next[10]依然是0。
public static int[] kmpNext(String pattern) {
//这个函数就是用来记录模式串的每个子串最大公共元素长度
int[] next = new int[pattern.length()];
next[0] = 0;//一个字符构成的子串没有公共前缀后缀
int j = 0;
for(int i = 1; i < pattern.length(); i++) {
if (j > 0 && pattern.charAt(j) != pattern.charAt(i)) {
j = next[j - 1];
}
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
有了next数组之后我们来看模式串如何移动?(移动多远)
因为模式串中首尾可能会有重复的字符,所以匹配失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值。
同理此图也可以沾到画图板上自己进行演示,这样可能会更容易理解。
下图就是要开始移动模式串了。
如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,下面进行字符串的匹配
这个图也可以供你们去自己演练一遍,小白就是要不断的去演练,自己明白,才能印象深刻。
过程如下图所示:
1. 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符A跟文本串的第5个字符A匹配成功:
2. 继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。因为此时已经匹配的字符数为6个(ABCDAB),然后根据《最大长度表》(也就是next数组存储的数据)可得失配字符D的上一位字符B对应的长度值为2,所以根据之前的结论,可知需要向右移动6 - 2 = 4 位。
3. 模式串向右移动4位后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的最大长度值为0,所以向右移动:2 - 0 =2 位。
4.A与空格失配,向右移动一位。
5. 继续比较,发现D与C 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的最大长度2,即向右移动6 - 2 = 4 位。
6. 经历第5步后,发现匹配成功,过程结束。
代码展示:
public static int kmp(String str, String pattern,int[] next) {
for(int i = 0, j = 0; i < str.length(); i++){
if (j>0 && str.charAt(i) != pattern.charAt(j)) {
i = i-next[j-1];
//此处的next[j-1]与next数组中的next[j-1]还是有差别的,这里是模式串右移时减去的当
//前字符之前一个字符的字符串的前缀和后缀最大相同长度。
j = 0;
}
if (str.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == pattern.length()) {
return i-j+1;
}
}
return -1;
}
最终代码展示:
import java.util.Arrays;
public class Test{
/*
str 文本串
pattern 模式串
next 记录当前字符之前的字符串有多大公共长度的前缀后缀
*/
public static void main(String[] args){
String a = "ABCDABD";//pattern
String b = "BBC ABCDAB ABCDABCDABDE";//str
int[] next = kmpNext(a);
System.out.println(Arrays.toString(next));//打印next数组
int res = kmp(b, a,next);
System.out.println(res);//打印该子串在文本串初始位置的角标。
System.out.println(b.substring(res,res+a.length()));
}
public static int kmp(String str, String pattern,int[] next) {
for(int i = 0, j = 0; i < str.length(); i++){
if (j>0 && str.charAt(i) != pattern.charAt(j)) {
i = i-next[j-1];
j = 0;
}
if (str.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == pattern.length()) {
return i-j+1;
}
}
return -1;
}
public static int[] kmpNext(String pattern) {
//这个函数就是用来记录模式串的每个子串最大公共元素长度
int[] next = new int[pattern.length()];
next[0] = 0;//一个字符构成的子串没有公共前缀后缀
int j = 0;
for(int i = 1; i < pattern.length(); i++) {
if (j > 0 && pattern.charAt(j) != pattern.charAt(i)) {
j = next[j - 1];
}
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
}
总结重点:
通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的字符串的前缀和后缀公共部分的最大长度后,便可基于此算法实现匹配。这个最大长度也正是next 数组要表达的含义。
如果对你有帮助,请给小白博主点个赞,让博主更加有信心去努力。一起加油吧!!!!