每日吐槽,KMP算法绝对是我见过最难的算法之一了,不知道是不是自己脑子不够用,还是咋滴,又或是最近太放飞自我了——学习1小时,玩手机一上午,我竟然看了整整两天才把这个算法看懂,嗷呜,不说了不说了,默默流下不争气的眼泪!

书看了好几遍,视频也听了好几个,然而我还是无法领会这神秘的KMP算法,没办法丫,我只好在那疯狂的看博客,看了一篇又一篇,最后,终于找到了一篇我能看懂的,没错,就是下面这篇博文,让我彻彻底底理解了KMP算法:
链接:https://www.cnblogs.com/yjiyjige/p/3263858.html
感谢博主,好人呐!简直就是我的救命恩人。
1.算法引入:
难归难,但不得不说,KMP算法真的是非常天才的一段编程,让我看看这是who想出来的,哦原来是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的。而KMP刚好就是三位大神的名字组合,我直呼好家伙!确认过眼神,都是我仰慕的人儿。
学过算法的人应该都知道,KMP是一个知名度很高的算法,几乎所有的《数据结构》书中都会提到它,那么现在问题来了,KMP算法是干什么用的呢?为啥它这么出名呢?
kmp算法又叫“看毛片”算法,是一个效率非常高的字符串匹配算法,它要解决的问题是求解模式串(也叫子串,称它为P)在主串(称它为T)中的位置。给定两个字符串,判断其中一个字符串是否包含另一个字符串,如果包含,则返回被包含串在包含串中的起始位置。
对于这个问题,有一种很常规的求解思路,那就是暴力枚举,从主串的第一个字符开始,从左到右依次进行匹配,在这个过程中如果存在某个字符不匹配,则将模式串向右移动一位,重新进行匹配。
算法过程如下:
首先初始化计数指针i和j,用i和j分别指示主串T和模式串P中当前正在进行比较的字符
依次比较i和j指向的字符是否一致,若一致,则i和j同时向右移动,继续比较下一个字符
若i和j指向的字符不一致,如上图中A和E不一致,则模式串P向右移动一位,指针i回到下标为1的地方,指针j回到下标为0的地方,继续进行比较
根据以上分析,我们可以写出下面的程序:
int match(string T, string P) {
int i = 0, j = 0;
while (i < T.length() && j < P.length()) {
if (T[i] == P[j]) {
i++;
j++;
}
else {
i = i - j + 1;
j = 0;
}
}
if (j == P.length()) return i - j;
else return -1;
}
暴力枚举算法能够解决字符串的匹配问题,但它绝不是一个好的算法,指针的频繁回退造成了大量不必要的字符比较操作,以致于它的时间复杂度达到了O(mn),而KMP算法在暴力枚举算法的基础上,进行了优化和改进,将时间复杂度降到了O(m+n)

2.KMP算法
对于暴力匹配算法,在第一趟匹配过程中,i=3处的字符与j=3处的字符不一致,于是又从i=1,j=0处重新开始比较,但是仔细观察,我们会发现,i=1,j=0和i=2,j=0这两趟比较是无需进行的,为什么呢?因为假如让我们人为寻找的话,我们就不会这样做,匹配失败的字符A之前的三个字符都是匹配的,除了第一个字符,就没有字符A了,那么即使将指针i移动到下标1和下标2处,分别与模式串中的第一个字符进行匹配,匹配也必将以失败告终。
这时我们脑海中会产生这样一种想法:保持指针i不动,只移动指针j至下标为0处,进行字符的比较操作,这样就可以避免指针i的无用回溯,从而降低算法的时间复杂度,而这便是KMP算法的思想:“利用已经部分匹配这个有效信息,保持指针i不回溯,通过修改指针j,让模式串尽可能移动到有效的位置”。
那么,当遇到模式串中的字符与主串中的字符不匹配的情况时,我们应该将指针j移动到哪里呢?
接下来,让我们来思考:
字符C和D不匹配了,我们应该将j移动到哪呢?那当然是下标1处丫,因为前面有一个字符A相同
同样下图也是一样的道理,很容易发现,应把j移动到下标2处,因为在此之前,有两个字符相同
仔细观察上面两个例子的P中失配字符之前的字符串,我们会发现,它有着相同的前后缀,假如一个字符发生失配后,j要移动到k,那么满足以下性质:下标k处之前的k个字符,和下标j处之前的k个字符是相同的,用公式表达如下:
P[0~k-1] = P[j-k~j-1]
那么即是当满足条件 P[0~k-1] = P[j-k~j-1] 时,可将指针j移动到下标k处,证明如下:
当 **T[i] != P[j]**时
有 T[i-j ~ i-1] == P[0 ~ j-1]
由 P[0 ~ k-1] == P[j-k ~ j-1]
必然有:**P[0 ~ k-1] == T[i-k ~ i-1] **
在模式串P的每一个位置处都有可能发生失配的情况,而每一个位置j对应一个k,这样就会产生多个k了,通常我们用一个next数组来存储这些k,next[j]=k,表示j的下一个位置是k,那么重点来了,怎么求出这些k值呢?我认为next数组的求解是整个KMP算法最关键的地方,是重点,也是难点,我当时就是卡在这个地方了,所以一直搞不懂kmp算法到底在讲个啥?
既然写不出来代码,那咱们就先来看代码好了
void get_next(string P, int * next) {
int j = 0, k = -1; //next[j]=k
next[0] = -1;
while (j < P.length() - 1) {
if (k == -1 || P[j] == P[k]) {
next[j + 1] = k + 1;
j++;
k++; //这三行等同于 next[++j] = ++k;
}
else {
k = next[k]; //寻找较短的相同前后缀
}
}
}
对于next[j]的求解,我们先来考虑边界情况:
j=0失配时,怎么办?此时j已经在最左边了,不能再向左边移动了,这时将i向右移动,初始化 next[0]=-1
j=1失配时,又该怎么办呢?很显然,j只能移动到下标0处,此时初始化 next[j]=0
那么一般的情况,已知 next[j]=k,我们应该怎么求next[j+1]呢?
next[j]=k,那说明 P[0~k-1] = P[j-k~j-1],此时让我们求 next[j+1],我们会很容易想到分两种情况讨论,哦豁,这怕是在做数学题,哈哈哈,我忍不住优雅的笑了!
当 P[k] = P[j]时,有P[0~k] = P[j-k~j],那么此时 next[j+1] = k +1,也即是 next[j+1] = next[j] +1,如果不信的话请看下面的图,正所谓有图有真相嘛
当 P[k] != P[j] 时,比如下图所示:
我们已经不可能找到 ABAB 这个最长的相同前后缀了,那么此时我们的目标就应该转向寻找较小的相同前后缀,因为我们还是可以找得到像 AB这样的相同前后缀,这样我们就能明白为什么 P[k] != P[j] 时,k = next[k]了,图示中,k = next[k] = 1,P[k]之前的A和P[j]之前的字符A相同,这很容易证明,此时若有P[k] == P[j],那么必有 next[j+1] = k + 1,简直完美,perfect!

至此,最难的next数组求解完毕,nice ! 接下来就是kmp匹配过程的代码了,小事情!不过要时刻记得一句话:next[j]=k,表示当模式串中第j位的字符与主串不匹配时,就把指针 j移动到下一个位置k下标处。好了,开始码代码啦!
//kmp算法求index
int kmp_index(string T, string P, int next[]) {
int i = 0, j = 0;
while (i < T.length() && j < P.length()) {
if (j == 0 || T[i] == P[j]) {
i++; j++;
}
else {
j = next[j];
}
}
if (j == P.length()) return i - j;
else return -1;
}
Game over,生活不易,喵喵叹气!
