参考的原文:详解KMP算法
KMP:解决字符串匹配问题,字符串s是否是主串的子集?是,返回第一个元素坐标;否,返回-1。
核心思想,尽量不移动主串的i指针,回溯子串的j指针。
整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪?
接下来我们自己来发现j的移动规律:
如图:C和D不匹配了,我们要把j移动到哪?显然是第1位。为什么?因为前面有一个A相同啊:
如下图也是一样的情况:
可以把j指针移动到第2位,因为前面有两个字母是一样的:
至此我们可以大概看出一点端倪,当匹配失败时,j要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的。
如果用数学公式来表示是这样的
P[0 ~ k-1] == P[j-k ~ j-1]
如图
至此得出一个问题:为什么能把j指针移动到k位置上
首先,当T[i] != P[j]时,必有T[i-j ~ i-1] = P[0 ~ j-1]
因为K必定小于j,所以得出 T[i-k ~ i-1] = P[j-k ~ j-1]
又因K满足P[0 ~ k-1] = P[j-k ~ j-1]
得 T[i-k ~ i-1] = P[0 ~ k-1]
所以能把j指针移动到k位置
关于K的描述还有前缀集和后缀集:
该规律是KMP算法的关键,KMP算法是利用待匹配的子串自身的这种性质,来提高匹配速度。该性质在许多其他中版本的解释中还可以描述成:若子串的前缀集和后缀集中,重复的最长子串的长度为k,则下次匹配子串的j可以移动到第k位(下标为0为第0位)。我们将这个解释定义成最大重复子串解释。
这里面的前缀集表示除去最后一个字符后的前面的所有子串集合,同理后缀集指的的是除去第一个字符后的后面的子串组成的集合。举例说明如下:
在“aba”中,前缀集就是除掉最后一个字符’a’后的子串集合{a,ab},同理后缀集为除掉最前一个字符a后的子串集合{a,ba},那么两者最长的重复子串就是a,k=1;
在“ababa”中,前缀集是{a,ab,aba,abab},后缀集是{a,ba,aba,baba},二者最长重复子串是aba,k=3;
在“abcabcdabc”中,前缀集是{a,ab,abc,abca,abcab,abcabc,abcabcd,abcabcda,abcabcdab},后缀集是{c,bc,abc,dabc,cdabc,bcdabc,abcdabc,cabcdabc,bcabcdabc},二者最长重复的子串是“abc”,k=3;
详见KMP详解 第二节
求next数组
先贴代码
public static int[] getNext(String ps) {
char[] p = ps.toCharArray();
int[] next = new int[p.length];
next[0] = -1;
int j = 0;
int k = -1;
while (j < p.length - 1) {
if (k == -1 || p[j] == p[k]) {
next[++j] = ++k;
} else {
k = next[k];
}
}
return next;
}
可以发现next[0] = -1,next[1] = 0必定成立。
先来看第一个:当j为0时,如果这时候不匹配,怎么办?j不动,移动i
再看第二个:j=1时,前面只有一个元素,所以只能回滚到这个坐标
最后有两种情况要重点讨论:
1.P[k] == P[j]
2.P[k] != P[j]
分别讨论
1.P[k] == p[j]时
当指针指在j时有个前提条件:P[0 ~ k-1] == P[j-k ~ j-1]
因为P[k] == P[j] 所以 P[0 ~ k] == P[j-k ~ j]
因此next[j+1] == k + 1 == next[j] + 1
2.P[k] != P[j]
代码中写的是 k = next[k] 如何理解 (最难理解的点)
当指针指向j时,有P[0~k-1] = P[j-k ~ j-1]
当指针指向j+1时,设此时的k为k1,则有P[0 ~ k1-1] = P[j+1-k1 ~ j]
因为P[j] != P[k] 所以 k1 <= k
那么k1具体等于多少呢?不知道,先从最大值k开始排查。
当k1=k时,有P[0~k-1] = P[j+1-k ~ j]
分解P[0~k-2] + P[k-1] = P[j+1-k ~ j-1] +P[j]
又P[j+1-k ~ j-1] = P[1 ~ k-1]
所以再分解为:
①P[0~k-2] = P[1 ~ k-1]
(这就是求next[k]时最先判断的字符串,)
②P[k-1] = P[j]
①式如果不成立,那么需要令k1=k-1,还得再判断一遍。不如跳过这些无用的假设值,直接让①式成立,去判断②成不成立。
如此便推出了代码中的k = next[k] 再判断P[k] == P[j]
如果P[k] == P[j]不成立,那么再来k1<= next[k],属于迭代的问题,再令k=next[k],继而推出代码
while(...){
if ( k == -1 || P[j] == P[k]){
P[++j] = ++k;
}else {
k = next[k];
}
}
有了Next数组后,就可以写出完整的KMP算法了
public static int KMP(String ts, String ps) {
char[] t = ts.toCharArray();
char[] p = ps.toCharArray();
int i = 0; // 主串的位置
int j = 0; // 模式串的位置
int[] next = getNext(ps);
while (i < t.length && j < p.length) {
if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0
i++;
j++;
} else {
// i不需要回溯了
// i = i - j + 1;
j = next[j]; // j回到指定位置
}
}
if (j == p.length) {
return i - j;
} else {
return -1;
}
}
以上算法还有优化,详见开头链接