我们会在面试或者日常“搬砖”过程中遇到这类问题:有一个文本串S(比如“ababbbaccdddmmd”),有一个模式串Q(比如“baccdd”),判断模式串Q是否是S的字串,如果是返回Q在S中的起始位置,如果不是返回-1。我们脑海里第一个思路就是循环遍历,如果当前字符匹配成功就继续匹配下一个,否则S中的标记向后移动一位,Q的标记回到最开始,就会有如下代码:
public static int kmpMatch(char[] chars, char[] ch) {
//S的长度
int sLen = chars.length;
//Q的长度
int qLen = ch.length;
int i = 0, j = 0;
while (i < sLen && j < qLen) {
if (chars[i] == ch[j]) {
i++;
j++;
} else {
//这里是把i回到这一轮的开始位置后再向后移动一位
i = i - j + 1; // i - ( j - 1 )
j = 0;
}
}
if (j == qLen) return i - j;
else return -1;
}
这个解法是正确的,但是我们是否在每次匹配失败的时候都需要另Q的标志位都回到0呢?显然是不需要,这就引入了KMP算法。
KMP算法和常规的暴力算法的区别就在于当S的字符和Q的字符不匹配时候的标记符号的移动,暴力解法是另i = i - j + 1,j = 0。在KMP算法中的核心思想是找到字符串Q中的前缀和后缀中的最长公共长度,让i不变,j移动最长公共字串的长度,这样就可以有效的避免了重复字符的移动,所以只需要求出j的吓一跳的数组就可以了我这里说的比较简单,我特意找了我当时学习时的一篇博客供大家学习 KMP详解,回到本文,什么是最长公共前后缀的长度呢?
以下说明以“ABCDABD”为例进行说明:
-"A"的前缀和后缀都为空集,共有元素的长度为0;
-"AB"的前缀为[A],后缀为[B],共有元素的长度为0;
-"ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
-"ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
-"ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
-"ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
-"ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素长度为0。
所以就得到了这样一个数组next[7]{0,0,0,0,1,2,0},所以每次只需要另j移动next[k]位就可以了,k就是每次不匹配字符的位置。于是,代码就可以写成如下:
private static int kmpMatch(char[] chars, char[] ch) {
int sLen = chars.length;
int pLen = ch.length;
int[] next = getNextArray(ch);
for (int i : next) {
System.out.print(i+" ");
}
int i = 0, j = 0;
while (i < sLen && j < pLen) {
if (chars[i] == ch[j]) {
i++;
j++;
} else {
j = next[j];
}
}
if (j == pLen) return i - j;
else return -1;
}
public static int[] getNextArray(char[] sub) {
int[] next = new int[sub.length];
next[0] = -1;
next[1] = 0;
int k;
for (int j = 2; j < sub.length; j++) {
//System.out.println(j);
k = next[j - 1];
while (k != -1) {
if (sub[j - 1] == sub[k]) {
next[j] = k + 1;
break;
} else {
k = next[k];
}
next[j] = 0;
}
}
return next;
}
通过getNextArray函数就可以获得模式串Q的下一跳位置的数组,这样就得到了KMP算法的代码。如果还有什么不明白的可以留言,我会进行解答的。