一、实现原理
例如有母串s[100] = {“abcababcab…”},有子串t[6] = {“abcabx”}。
前5 个子串与母串都是相等的,当比较进行到第六位时,子串与母串不相等。
常规的思路是让子串对齐母串的第二位再进行遍历。
BUT,聪明的克努特、莫里斯、普拉特采用了一种很聪明的算法,使得时间复杂度远远下降,实现原理如图(废话好多):
就是这么个原理,然后加上亿点点细节如下:
所以实现其算法的关键之一就在于找到子串的相等的前后缀,即next数组
二、基本kmp
可参见视频https://www.bilibili.com/read/cv8013121
讲的比较详细
1、next数组
首先吐槽大话数据结构这本书,讲的kmp中t[0]用来存长度,不符合我们C语言学习者的习惯。
所以下面采用更通俗易懂的正常的方法,由前面的基础可知next数组里j值的大小完全取决于当前字符之前的相似度。
计算next[j]的方法:
- 当j = 0时,next[j] = -1;//沙比大话数据结构j = 0时令为0.
- 当j>0时,next[j]的值为:模式串的位置从0到j-1构成的串中所出现的首位相同的的字串的最大长度。
- 当没有首位相同的子串时next[j]的值为0。
如图:
接下来把他变成代码
#include <stdio.h>
#include <string.h>
void get_next(char *t,int *next)
{
int i = 0,j = -1;
int len;
next[0] = -1;
len = strlen(t);
i = 1;//后缀
j = 0;//前缀
next[1] = 0;
while(i<len)
{
if(j == -1|| t[i] == t[j])//当j回溯到-1时,j从头开始,i继续加一
{
++j;
++i;
next[i] = j;//最精彩的地方来了 下面会讲
}
else
{
j = next[j];//女少口啊 下面会讲
}
}
/*for(int i = 0;i<len;i++)
{
printf("%d",next[i]);
}测试用*/
}
对于这一语句的理解是
next[i] = j;
next[i]在i位之前的最长公共缀值 = j,j的值表示相同前缀的字符个数。(沙比大话里面j初始化为0,此时j表示为前缀的长度+1)
大的要来了
else//t[i]!=t[j]
{
j = next[j];//回溯
}
next[j]是一个已知的值,以为都已经算到next[i]了,而i是比j要大的(我当时为什么要纠结这个,好蠢)。
这一语句表示什么意呢?就是j要回退到曾经找过的公共缀位置,继续比较。
那为什么next里面下标是j呢?因为j为前缀,此时前缀字符与后缀字符不等,故需要回溯到第j位之前的最长公共缀值,再从那个位置重新开始匹配。(真的佩服这三位大佬,怎么想到的)
2、全部代码实现
直接上代码
#include <stdio.h>
#include <string.h>
int next[1000];
void get_next(char *t,int *next)
{
int i = 0,j = -1;
int len;
next[0] = -1;
len = strlen(t);
i = 1;//后缀
j = 0;//前缀
next[1] = 0;
while(i<len)
{
if(j == -1|| t[i] == t[j])//当j回溯到-1时,j从头开始,i继续加一
{
++j;
++i;
next[i] = j;//最精彩的地方来了 下面会讲
}
else
{
j = next[j];//女少口啊 下面会讲
}
}
/*for(int i = 0;i<len;i++)
{
printf("%d",next[i]);
}测试用*/
}
int kmp(char *s,char *t)
{
int i = 0,j = 0;
int len_s,len_t;
len_s = strlen(s);
len_t = strlen(t);
while(i<len_s&&j<len_t)
{
if(j == -1||s[i] == t[j])//若j==-1则从头再来
{
i++;
j++;
}
else
{
j = next[j];//精彩的部分
}
}
if(j == len_t)
{
return (i-j);//返回子串t在母串第几个字符之后的位置
}
else
{
return -1;//没有则返回-1
}
}
int main()
{
char s[1000],t[1000];
int res;
scanf("%s",s);
getchar();
scanf("%s",t);
get_next(t,next);
res = kmp(s,t);
printf("%d",res);
return 0;
}
对于j = next[j];
//是不是有些眼熟?形式与next函数中的一样,其功能也差不多
就是将j向右滑动到next[j]的位置
运行截图
三、加强版kmp
1、 为什么会有加强版??
因为1.0版本不够强。
进入正文,当s = ”aaaabcde“,子串t = “aaaaax”,此时就会出现很多重复的比较,如图(图源《大话数据结构》由于大话里j的初始值不一样,且对数组的定义也不一样,所以看懂意思就行,下标什么的不必纠结)
就是当子串中有许多相同的字符时会产生很多重复的判断
nextval数组及完整代码
就真的只是加了一点点细节。
#include <stdio.h>
#include <string.h>
int nextval[1000];
void get_nextval(char *t,int *next)
{
int i = 0,j = -1;
int len;
next[0] = -1;
len = strlen(t);
i = 1;//后缀
j = 0;//前缀
next[1] = 0;
while(i<len)
{
if(j == -1|| t[i] == t[j])//当j回溯到-1时,j从头开始,i继续加一
{
++j;
++i;
if(t[i]!=t[j])
{
nextval[i] = j;
}
else
{
nextval[i] = nextval[j];//如果与前缀字符相同,则将前缀字符的nextval值赋给nextval在i位置的值
}
}
else
{
j = nextval[j];
}
}
/*for(int i = 0;i<len;i++)
{
printf("%d",next[i]);
}测试用*/
}
int kmp(char *s,char *t)
{
int i = 0,j = 0;
int len_s,len_t;
len_s = strlen(s);
len_t = strlen(t);
while(i<len_s&&j<len_t)
{
if(j == -1||s[i] == t[j])//若j==-1则从头再来
{
i++;
j++;
}
else
{
j = next[j];//精彩的部分
}
}
if(j == len_t)
{
return (i-j);//返回子串t在母串第几个字符之后的位置
}
else
{
return -1;//没有则返回-1
}
}
int main()
{
char s[1000],t[1000];
int res;
scanf("%s",s);
getchar();
scanf("%s",t);
get_nextval(t,next);
res = kmp(s,t);
printf("%d",res);
return 0;
}
其实我感觉也没加强多少
四、 扩展版kmp
扩展版kmp的用处
当子串与母串不完全匹配时,我们也想知道母串的每个位置最多有几个字符与子串相等,或者我们想知道字串在母串中出现了几次,就需要用到此法
我们用extend[i]
来表示s[i...|s|]
与t的最长公共前缀的长度
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
s | a | b | a | b | a | c | a |
t | a | b | a | c | |||
extent[i] | 3 | 0 | 1 | 4 | 1 | 0 | 1 |
实现思路
假设当前遍历到s串位置i,extend[i]的值已经得到,l和r,r代表以l为起始位置的字符匹配成功的右边界,r = 最后一个匹配成功的位置+1,即s[l…r]等于t[0…r-l]。
现在我们还需要一个辅助数组next[i],定义为表示t和t[i…|t|]的最长公共前缀,其实就是extend[i]数组的子串和母串都是t的情况
如下表
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
t | a | b | c | a | b | c | a | b | d |
next | 8 | 0 | 0 | 5 | 0 | 0 | 2 | 0 | 0 |
①当i+next[i-l]<r
时
可以看出next[i-r]即为从i-r开始的t的最长前缀,图中用圆圈表示。所以此时extend[i] = next[i-r]
②当i+next[i-l]=r
时
处肯定不等于r-i+1(B)处,所以next数组才定在了r-l处。
又因为s中r+1(C处不等于r-l+1(B)处,但是不能判断C等不等于(A)。
故接下来就从s[r]和t[r-l]处开始继续往后遍历。
③当i+next[i-1]>r
时
可以肯定的是A部分==B部分。
又因为a部分!=b部分所以C部分!=B部分。
故C部分不等于A部分。
此时extend[i] = r-i。
代码实现
直接上代码不多bb
#include <stdio.h>
#include <string.h>
void GetNext(char *T, int m, int *next)//注释参考后面的
{
int l = 0, r = 0;
next[0] = m;//下标为0时next值就等于数组长度
for (int i = 1; i < m; i++)
{
if (i >= r || i + next[i - l] >= r)// i >= r 的作用:举个典型例子,T和 T 无一字符相同
{
if (i >= r)
r = i;
while (r < m && T[r] == T[r - i])
r++;
next[i] = r - i;
l = i;
}
else
next[i] = next[i - l];
}
}
/* 求解 extend[] */
void GetExtend(char *S, int n, char *T, int m, int *extend, int *next)
{
int l = 0, r = 0;
GetNext(T, m, next);
for (int i = 0; i < n; i++)
{
if (i >= r || i + next[i - l] >= r) // i >= r 的作用:举个典型例子,S 和 T 无一字符相同
{
if (i >= r)//i>r时,r落后要跟上i的步伐
r = i;
while (r < n && r - i < m && S[r] == T[r - i])//匹配则r加一
r++;
extend[i] = r - i;
l = i;//结合图片
}
else
extend[i] = next[i - l];
}
}
int main()
{
int next[100];
int extend[100];
char S[100], T[100];
int n, m;
gets(S);
gets(T);
n = strlen(S);
m = strlen(T);
GetExtend(S, n, T, m, extend, next);
for(int i = 0;i<m;i++)
{
printf("%d",next[i]);
}
printf("\n");
for(int j = 0;j<n;j++)
{
printf("%d",extend[j]);
}
return 0;
}
完工