KMP算法
KMP算法是一种改进的字符串匹配算法。核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。
算法设计
特点
主串的i不会回退,并且j也不会移动到0号位置。
主串i
为什么不回退?
- 如上图情况,假设在2处匹配失败,就算主串退回到1位置,这个位置的b和子串0位置的a仍然不相等,非常浪费比较次数。
子串j
回退的位置?
- 当匹配失败时,我们不将主串
i
进行回退。因为在这个地方匹配失败说明i
的前面和j
的前面有一部分是相同的。 - 结合上图分析,当j回退到
2
下标时,i
不回退,就是最好的情况。 - 为什么会这样回退呢?大致分析可知:我们找到
i
位置前面和子串0
位置开始有没有已经匹配的子串。我们可以发现子串0,1
位置a,b
与i
之前3,4
位置a,b
已经匹配,因此将j
退回子串2
位置即可。
既然我们得到了最佳的回退方案,所以我们可以使用一个数组来存储子串每次匹配失败最佳回退位置,这样就能利于我们匹配了。
next数组
KMP的精髓就
next
数组,它用next[i] = k
来表示。不同的j
来对应一个k
值,这个k
就是匹配失败后j
要移动的位置。
k值的求法
- 找到匹配成功部分的两个相等的真子串(不包含本身),一个以下标
0
开始,另一个以下标j-1
结尾。 - 不管什么数据
next[0] = -1; next[1] = 0;
这里我们以下标开始,第几个都是从1开始算。
用上面的方法求next数组
练习1:
子串: a b a b c a b c d a b c d e
next数组:-1 0 0 1 2 0 1 2 0 0 1 2 0 0
练习2:
子串: a b c a b c a b c a b c d a b c d e
next数组:-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0
- 通过上面的练习我们可以看出next数组的值增加时一定是连续的,不会产生跨度,这点可以检查正确性。
递推公式
通过上面我们学会了
next[i] = k
的求法,但是想要获得整个数组的值,我们能不能由next[i] = k
求出next[k+1]
的值呢?如果可以实现那么整个数组就变成递推可求的了。
- 假设1:
next[i] = k
成立,那么p[0]...p[k-1] = p[i-k]...p[i-1]
。 - 假设2:
p[i] = p[k]
成立,那么p[0]...p[k] = p[i-k]...p[i]
。 - 由上述条件可得:
next[i+1] = k+1
。
- 当假设2不成立时,也就是
p[i] != p[k]
时,回退到k位置仍然不相等,所以我们应该一直回退,直到找到p[i] = p[k]
为止。
代码实现
c语言
初始next数组前两项为-1,0
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
void getNext(char* sub, int* next, int lenSub) {
next[0] = -1;
next[1] = 0;
//当前i的下标
int i = 2;
//存储的前一项的k值。
int k = 0;
while (i < lenSub) {
if (k == -1 || sub[i - 1] == sub[k]) {
next[i] = k + 1;
i++;
k++;
}
else {
k = next[k];
}
}
}
int KMP(char* str, char* sub, int pos) {
assert(str && sub);
int lenStr = strlen(str);
int lenSub = strlen(sub);
if (lenStr == 0 || lenSub == 0) {
return -1;
}
if (pos < 0 || pos >= lenStr) {
return -1;
}
//求next数组
int* next = (int*)malloc(sizeof(int) * lenSub);
assert(next);
getNext(sub, next, lenSub);
//遍历主串
int i = pos;
//遍历子串
int j = 0;
while (i < lenStr && j < lenSub) {
if (j == -1 || str[i] == sub[j]) {
i++;
j++;
}
else {
j = next[j];
}
}
free(next);
if (j >= lenSub) {
return i - j;
}
else {
return -1;
}
}
int main() {
printf("%d\n", KMP("abcababcabc", "abcabc", 0));
}
java
初始next数组前两项为0,1
import java.lang.reflect.Array;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
char[] t = "ABABABAAABABAA".toCharArray();
char[] p = "ABABAAABABAA".toCharArray();
int[] next = new int[p.length];
int i = 1, j;
//next数组的创建
while(i < next.length){
j = i - 1;
while(true){
if(next[j]==0 || p[i-1] == p[next[j]-1]){
next[i] = next[j] + 1;
break;
}
j = next[j] - 1;
}
i++;
}
//进行字符串匹配
i=0;
j=0;
while(i<t.length){
if(t[i] == p[j]){
i++;
j++;
}
else{
if(j==0){
i++;
}
else{
j = next[j] - 1;
}
}
if(j == p.length){
System.out.println("匹配成功!");
break;
}
}
}
}
算法分析
时间复杂度:O(m+n)