KMP算法简介:
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
找子串同样也有逻辑思路简单的的BF算法 ,也就是暴力解法,如下图所示:
我们有主串ABCABCDHIJK 和子串 ABCE
BF算法是先从两串的第一个元素开始判断元素是否相等:
如果元素相等的话就接着判断两串的下一个元素,如果遇见不相等的情况的时候如下图:
遇到上图的情况时,就让i回溯到最初位置的下一个位置此处为下标为1的位置,让j回溯到下标为0的位置,然后又一个一个地判断元素是否相同,如下图:
上面就是BF算法的大体逻辑,十分简单,但是我们很明显可以发现这样的算法时间复杂度有可能会很高,假设主串长度为n,子串长度为m,最高时间复杂度为O(m*n),最低为O(m),但是可想而之一般情况下的时间复杂度都会比较高的。
KMP算法就是为了解决这一问题的,KMP算法的主要优点就是省去了主串的回溯过程,增加一点空间复杂度(空间复杂度为O(m))来大幅降低时间复杂度,逻辑如下图:
首先我们先从两串的第一个元素开始逐个判断两元素是否相同,遇到两元素不同的情况如下图:
遇到上图的情况时,我们进行的操作是判断子串j下标前的字符串中的最长相等前后缀,此处的最长相等前后缀为AB,至于最长相等前后缀的概念我们下面马上讲解,找到最长相等前后缀后,我们的操作是让主串的i不变,子串的j指向最长相等前缀的下一个元素,然后继续与主串的i下标的元素继续开始逐个比较,如下图:
我们发现,这样就可以直接跳过多次无用的比较,直接有用的地方开始比较。
最长相等前后缀是什么:
我们以字符串abccab为例:
前缀的集合为:{a, ab, abc, abcc, abcca}
后缀的集合为:{b, ab, cab, ccab, bccab}
从上面两集合中我们可以清晰看到最长相等前后缀为ab
例如:cacac的最长相等前后缀为cac
然而,也有最长相同前后缀不存在的情况,如:abcbc
了解了什么是最长相等前后缀,我们就可以接着了解KMP算法的灵魂next数组了,由于一个字符串中的每一个字符前的字符串都有可能有最长前后缀,而且最长相等前后缀的长度是我们移位子串去匹配主串的关键,所以我们单独用一个next数组存储子串的每个元素前的字符串的最长相等前后缀的长度。
例如子串abcabc的next数组如下表所示:
a | b | c | a | b | c |
---|---|---|---|---|---|
next[0] | next[1] | next[2] | next[3] | next[4] | next[5] |
-1 | 0 | 0 | 0 | 1 | 2 |
其中next[0]为-1的原因是下标为0的a的前面没有元素可处理(没有前缀),其余next[]为0的情况是前缀中没有最长相等前后缀或最初长相等前后缀只是一个字符,为0的话就可以当与主串元素不符的时候j直接指向子串的第一个元素继续与主串比较。
总结next数组的作用:
- next[i]的值表示下标为i的字符前的字符串最长相等前后缀的长度。
- 表示该处字符不匹配时应该回溯到的字符的下标
next数组的代码如下:
代码中s为主串,t为子串
//串的数据结构如下:
typedef struct {
char data[MaxSize];
int length;//串长
} string;
//由模式串t求出next值
void GetNext(string t, int next[]) {
int j, k;
j = 0;
k = -1;
next[0] = -1;//第一个字符前无字符串,给值-1
//因为next数组中j最大为t.length-1,而每一步next数组赋值都是在j++之后
//所以最后一次经过while循环时j为t.length-2
while (j < t.length - 1) {
//k为-1或比较的字符相等时
if (k == -1 || t.data[j] == t.data[k]) {
//对应字符匹配情况下,s与t指向同步后移
j++;
k++;
next[j] = k;
} else {
//对k进行回溯
k = next[k];
}
}
}
其中,k在执行回溯前t.data[j]已经有了一对最长相等前后缀,然而,该最长前缀字符串中也有自身的最长相等前后缀,且与t.data[j]前的最长相等后缀自身的最长相等前后缀相同,而k的回溯实际上就是将k指向t.data[j]前的最长相等前缀自身的最长相等前缀的下一个元素。
详见此位大佬的博客:大佬博客
其中大佬博客中的图片讲解如下:
KMP完整代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
char data[100];
int length;//串长
} string;
//由模式串t求出next值
void GetNext(string t, int next[]) {
int j, k;
j = 0;
k = -1;
next[0] = -1;//第一个字符前无字符串,给值-1
//因为next数组中j最大为t.length-1,而每一步next数组赋值都是在j++之后
//所以最后一次经过while循环时j为t.length-2
while (j < t.length - 1) {
//k为-1或比较的字符相等时
if (k == -1 || t.data[j] == t.data[k]) {
//对应字符匹配情况下,s与t指向同步后移
j++;
k++;
next[j] = k;
} else {
//对k进行回溯
k = next[k];
}
}
}
//KMP算法
int KMPIndex(string s, string t) {
int next[100], i = 0, j = 0;
GetNext(t, next);
while (i < s.length && j < t.length) {
if (j == -1 || s.data[i] == t.data[j]) {
//i,j各加1
i++;
j++;
}
else {
//i不变,j后退,即重新让子串的相应元素匹配主串i位置的元素
j = next[j];
}
}
//如果j能成功走到子串的最后一个元素就证明成功匹配
if (j >= t.length) {
return(i-t.length);//返回匹配模式串的首字符下标
} else {
return -1; //返回1表示主串和子串不匹配
}
}
//主函数
int main (void) {
int x;
string s, t;
s = *(string *)malloc(sizeof(string));
t = *(string *)malloc(sizeof(string));
printf("请输入相应的主串和子串:\n");
scanf("%s%s", s.data, t.data);
s.length = (int)strlen(s.data);
t.length = (int)strlen(t.data);
x = KMPIndex(s, t);
if (x == -1) {
printf("error");
} else {
printf("子串第一个下标位置为:%d\n", x);
}
}
运行结果示例: