kmp算法理解
暴力匹配
原理
在字符串的匹配问题中,被匹配的字符串为”主串“,匹配的字符串为”模式串“,如果最终在主串中有模式串的出现,则返回其具体位置,反之则返回-1(遍历主串的指针为i,遍历模式串的指针为j)。
在暴力算法中,就是直接遍历主串的每个元素,将模式串中的元素一一匹配。
如该动图所示,该匹配过程为,从主串的第一个元素开始,依次遍历,直至匹配完成,此时返回匹配到的主串第一个元素位置。
代码
public static int bf(String ts, String ps) {
char[] t = ts.toCharArray(); //主串
char[] p = ps.toCharArray(); //模式串
int i = 0; // 主串的位置
int j = 0; // 模式串的位置
while (i < t.length && j < p.length) {
if (t[i] == p[j]) { // 当两个字符相同,就比较下一个
i++;
j++;
}
else {
i = i - j + 1; // 一旦不匹配,i后退
j = 0; // j归0
}
}
if (j == p.length) {
return i - j; //如果找到,返回在主串中第一个字符出现的下标
}
else {
return -1;
}
}
kmp算法
特点
分析暴力法我们发现,在每次匹配过程中,只要出现不匹配的情况,模式串都是整体和下一个主串元素开始匹配,在这过程中,我们没有从之前的匹配过程中总结规律以便进行之后的匹配,只是机械的遍历匹配,具体表现为:
- 主串将会回溯
- 模式串将会归零
那么kmp算法所做的就是处理这两处问题,简而言之,kmp算法的特点为:
- 利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置
分析实现
那么我们需要解决的问题也就清晰了:当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪
接下来分析j的移动规律:
C和D不匹配了,此时i不动,j将会移动到第二位"B"处与此时的"C"匹配
因为我们发现,i所指的前一位是"A"可以与模式串匹配
此时发现一条规律:当匹配失败时,j要移动的下一个位置k。最前面的k个字符和j之前的最后k个字符是一样的。
之前我们第一次成功匹配的是"ABA”,然后第二次成功匹配的是"A",此时j=2(如果第一位定义了j=0,那此时j=1),那么也就是说k=2,最前面的k个字符也就是"A"是和之前"ABA"中的第一个"A"是一样的
用公式表示:P[0—K-1] == P[J-K—J-1]
(j-k:之前匹配成功的主串中第一个元素位置 j-1:之前匹配成功的最后一位即匹配失败前一位)
next数组
上文已经搞明白我们是怎样减少无谓步骤的,那现在该怎么求这个(这些)k呢?
因为在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当主串与模式串不匹配,T[i] != P[j]时,j指针的下一个位置。
- 代码
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]) {
if (p[++j] == p[++k]) { // 当两个字符相等时要跳过
next[j] = next[k];
} else {
next[j] = k;
}
} else {
k = next[k];
}
}
return 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;
}
}