本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n) 。
暴力解法
当查询一个主串里面是否有这个模式串的时候,使用暴力解法是从头开始遍历主串,当主串中的字符与模式串的第一个字符对应的时候两个串就开始对比下一个,当中间有遇到不相等就需要回过头从主串一开始确定第一个的位置往下一个开始继续判断。
比如上面最后一个B和F不相等,就很可惜,需要掉头回来重新判断,继续从第一个开始。
为了避免这种可惜,就有了KMP算法。
KMP理论
说到底暴力解法只是机器的思考方式,当机器执行人的思考方式的时候,效率就会得到提高。如果是人脑进行思考,当B和F相等的话,我们直接就把模式串挪到这样的位置进行比较了。
如果觉得上面的例子不够充分,我们再举几个例子:
比如说上面的这种情况,如果匹配到这个位置不相同,人脑会傻乎乎的像机器一样一个个匹配吗?肯定是直接挪动到以下这个位置,再依次从第二个位置开始比。
那我们再来一个例子:
这种情况不匹配了应该怎么挪?
我们把上面的指针用i表示,下面的指针用j来表示。相信你也有点找到规律的感觉了,既然都比到i这个位置了,首先说明比较i和j现在位置i之前的和j之前的那些字符肯定相等,这是毋庸置疑的。如果在这个模式串里面有和这个j之前的某段字符相等那是不是就可以不用比较之前的了,直接再从不相等的一个开始比较呢?
主要是这个相等的前面那一段还必须是从头开始连着的相等,后面相等要从尾巴地方往前连着的,不然不好使。
下面就引出了新概念,最长相等前后缀。理解这个最长相等前后缀之前还要理解下前缀和后缀。下面以aabaa为例子:
前缀分别有:
- a
- aa
- aab
- aaba
- aabaa就不是它的前缀,因为前缀是不包含最后一个字符的。
后缀分别有:
- a
- aa
- baa
- abaa
- aabaa就不是它的后缀,因为后缀是不包含最前面那一个字符的。
再结合前面标红的那句应该就懂了这个最长相等前后缀的含义了,最长相等前后缀也就是aa。
比如说这是个aabaaf,到f这个位置上下i,j指向位置不匹配,就会寻找除了f以外的字符串(aabaa)的最长相等前后缀aa,下一次就会从b这个位置开始再进行比较。
这个最长相等前后缀就是我们人脑的真实表达。如果说到b这个位置也不匹配呢?
其实也是一样的道理,也是看B前面的字符串AA的最长相等前后缀,然后少比一点。
其实这种比来比去的时候,i这个指针指向的位置永远不变,变来变去的只有j指针的指向位置。
那么也就是说匹配的时候有可能第一个就不匹配了,有可能第二个就不匹配了,只要我们列一个数组存放着每种未知的可能就行,也就是next数组,这个数组是可以存多种形式的,都是不同人的不同写法。比如数组下标对应的值就是到这个值就不相等的话j应该指向的下标位置或者等等一系列别的意思。本质上都是指向最长相等前后缀的前缀的后面一个。因为数组最长相等前后缀的前缀都是从第一个字符开始连着的子串,那么这个最长相等前后缀的前缀的长度也代表着j应该指向的位置。
对于next数组我更喜欢让它就存放到当前字符为止的子串的最长相等前后缀的长度。这样当i和j不匹配的时候我就直接找next[j-1]。具体存放的数据因人而异,但本质上都和最长相等前后缀的长度脱不了干系。
求next数组
void getnext(vector<int>& next,string mode) {
int j = 0;
next[0]=0;
for (int i = 1; i < mode.size(); i++) {
while (j > 0 && mode[j] != mode[i]) {
j = next[j - 1];
}
if (mode[j] == mode[i]) {
j++;
}
next[i] = j;
}
}
这里我先放代码吧,这里的i和j代表的含义也不一样了。i代表后缀的末尾,j代表前缀的末尾。
主要就是分为四个步骤:初始化,i和j不相等的情况,i和j相等的情况,给next数组赋值。
首先进行初始化,因为只有一个字符的时候是没有前后缀的,也就没有什么最长相等前后缀的说法了,所以next[0]=0。(可以假设第二个不相等了,然后找前面子串的最长相等前后缀,直接就是0。那么也就是j指针直接移到0的位置开始进行比较,符合要求)那么i和j就要从字符串大小为2的时候开始初始化,这时候j指向0,i指向1。j也就初始化为0,i初始化为1。
先说说i和j相等的情况吧,比如aa,j代表a,i也代表a,那么相等j++,如果j++以及下一次循环的i值他俩所代表的字符还是相等的话,那么说明在原来相等的长度上继续加一,记得给next数组赋值。这样求出来的相等的情况代表着j和i之前的j前面子串和i前面部分子串都是相等的
注意上面标红字段,如果说遇到了不相等,我们肯定不想再重头慢慢找什么相等前缀后缀,这时候next数组i之前算出来的那些数组值就起到了关键作用。因为我们知道j和i之前的j前面子串和i前面部分子串都是相等的,我们就可以直接通过选择j之前的子串的最长相等前后缀,然后j移到值对应的位置(现在j前面的和i前面和j前面一样长度的子串是相等的,因为是相等前后缀),如果此时i和j指向的字符相同,那么next[i]直接就等于j对应的下标,然后又可以进入到那种上一次是相等的下一次循环之中,如果两个对应的还不相等,但是注意此时j前面和i前面部分的字符串还是相等的,还是可以如法炮制此方法,j前面的子串的最长相等前后缀还是有可能帮助你不重头慢慢找的可能性,只要值为0,那就没办法了,还不相等就只能赋值为0了。所以这里不是if而是while循环的原因。
注意这个j要>0,等于0的时候就没必要进入循环了,直接往下走,相等的话就直接+1再赋值,不相等的话直接就赋0。
实现代码
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) { //说明主串中有模式串了就可以直接return了
return (i - needle.size() + 1);return了
}
}
return -1;
}
参考
本文参考卡哥的代码随想录