感觉网上讲解的 KMP算法 看的我云里雾里的,而且还有很多公式,看着头大。
于是想着把自己理解的 KMP算法 写出来,以便之后忘记后能快速拾起。
我没有写数学理论,详解可以看LeetCode 28.实现 strStr() 官方解析
例题
简单来说,就是给两个 String 参数,主串 haystack, 模式串 needle。
现在,需要返回,如果在主串中存在子串,子串与模式串相等,那么返回子串首个字母在主串的位置。
如果没有这个子串,那么返回 -1.
全是小写英文字母。
空讲的感觉难以理解,那么还是举个栗子吧。
主串 haystack = “abaabecabaabcmmnr”
模式串 needle = “abaabcmmn”
关于模式串的前后缀匹配数组
这个数组是 KMP算法 的核心,说这个数组之前,需要了解一个概念。
前缀、后缀
前缀就是一个字符串从第一位开始向后,不到达最后一位的子串。对于模式串来说,“a”、“ab”、“aba”、…、“abaabcmm” 都是前缀,但是再多加一个 n 就不是前缀了。
后缀就是从字符串的非首位,必须到达最后一位的子串。对于模式串来说,“n”,“mn”,“mmn”、“cmmn”、…、“baabcmmn” 都是后缀。
匹配数组(next 数组)
前缀、后缀知道是什么了,那前后缀匹配就是前缀等于后缀呗。这个也好理解。
那么来计算一下,模式串从开始到结尾,遍历每个位置 i
时,从 0
到 i
这个子串的 最大前后缀匹配字数 是多少。
这个数组我们暂时叫它 fl int[] fl = new int[needle.length()];
,而且需要两个指针 i
j
,i
来指向后缀位置,j
来指向前缀位置。
1、 首先遍历第一位,i
是 0
,j
是 0
,此时的子串是 “a”
,无法产生前缀和后缀,所以 fl[0] = 0;
。
我们可以在代码中直接跳过这一步,直接让 i
处于 1
位置。
2、 当 i = 1,j = 0。此时的子串是 “ab”。
蓝色方块代表 i
的当前指向,向上箭头是 j
的当前位置指向,红色方块代表 needle[j] != needle[i]
。
前缀 “a”
与 后缀 “b”
不匹配,此时的 fl[1]
是 0
;没有匹配串。
3、 当 i = 2,j = 0。此时的子串是 “aba”。
蓝色方块代表i
的当前指向,向上箭头是j
的当前位置指向,绿色方块代表 needle[j] == needle[i]
。
前缀 “a”
与后缀 “a”
匹配,此时的fl[2]
是 1
.
此时,做一个设想,假如再多一位字母,这个字母恰好是 “b” ,也就是子串是 “abab”。
现在我已经知道,needle[j] == needle[i]
,那么,是不是只需要再判断 needle[j+1] == needle[i+1]
就可以了,前缀就不需要在从头一位位判断了。
好。。那我现在也就是说,当我遇到 needle[j] == needle[i]
,我就让j(前缀位置,箭头指向)
向后移动一位,用于i
再拓展一个字符后的比较。
但是,还有一种情况,假如多的这个字母不是 “b”,是 “c”,子串就成了 “abac”。
那么,这也就意味着,从 2
位置开始的后缀在长度为 4
的这个字符串中,是不能和前缀进行匹配的。很明显,2
位置开始的后缀 “ac”
不等于 同等字数的前缀 “ab”
。
通过这个图来看,我需要将后缀的开始位置向后调整一位,也就是用后缀 “c”
与前缀 “a”
去比较。也就是将j
前调一位,然后用当前位置的i
与 j
做比较。
再来看一种情况,看一个新的字符串
先不管前边怎么匹配的,就看现在的情况。
此时 i
指针指向的 "c"
不等于 j
指向的 "a"
。难道我要将i
回调到3
位置并且j
回调到0
位置吗?这个匹配数量还是短的,如果很长呢,那岂不是这么长的绿色匹配都做了无用功。而且i
j
都修改容易混乱。
能不能有一个只调整一个指针,并且回调步长没那么长的办法?
仔细观察绿色子串,发现其中也有一个前后缀匹配串 "ac"
,
那我可以直接把前缀"ac"
直接匹配在后缀"ac"
上啊,然后我将j
指针回调到2
位置,再用needle[i]
与needle[j]
进行比较。
而且整个过程只是 j
在调整位置,i
一直没动,
回调的位置也不是从头开始的,而且回调到 fl[j-1]
的位置,也就是 绿色子串的最大前后缀匹配字数。
此时还是不相等,那么继续我们这个步骤,
①将j
回调到fl[j-1]
的位置,注意j
要大于0
②比较回调之后,needle[j]
和needle[i]
是否相等
其实后边就不用再写了,到这里计算匹配数组的方法就可以出来了
int hlen = haystack.length(), nlen = needle.length();
// fl[n] 存储 模式串从0到n闭区间的子串 最大前后缀匹配子串的字数
int[] fl = new int[nlen];
for(int i = 1, j = 0 ; i < nlen ; ++i) {
while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
j = fl[j - 1];
}
if (needle.charAt(i) == needle.charAt(j)) {
++j;
}
fl[i] = j;
}
模式串匹配
主串、模式串匹配和计算匹配数组过程很像。
同样的,
用i
表示主串(上边字符串)的指针迭代位置(蓝色方块),
j
表示模式串(下方字符串)迭代位置(向上的箭头),
绿色代表匹配上了,
红色表示不匹配,
黄色是模式串中匹配的前后缀。
如图,此时两个指针字符不匹配了,我需要将模式串的0
位置去主串1
位置进行比较吗?
不用。
因为fl
存储的是最大的匹配串,所以直接用模式串匹配原串位置的后缀位置。即:
按照上边的步骤,
①当前位置不匹配,将j
回调到fl[j-1]
的位置,注意j
要大于0
②比较回调之后,needle[j]
和needle[i]
是否相等
之后的流程
for (int i = 0, j = 0 ; i < hlen ; ++i) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)){
j = fl[j - 1];
}
if(haystack.charAt(i) == needle.charAt(j)) {
++j;
}
if(j == nlen) return i - nlen + 1;
}
例题AC代码
class Solution {
public int strStr(String haystack, String needle) {
int hlen = haystack.length(), nlen = needle.length();
// 模式串的长度是0,直接0位置就匹配上了
if(nlen == 0) return 0;
// 模式串的前后缀最大相同匹配数量
int[] fl = new int[nlen];
// i:遍历到的位置 也就是后缀最后一个位置,j:前缀的最后一个位置,
for(int i = 1, j = 0 ; i < nlen ; ++i) {
// j==0时,这个循环可以先跳过,一会在看
// 好。此时的j大于0了,如果needle[j] == needle[i],去下边比较就好了,这里是用来缩短最长匹配串的。
// 如果两者不等于,进入循环,,
while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
// 此时,你要思考一下,是不是我们匹配后缀的时候是从 1 位置开始的,到现在的 i 位置,也就是说needle[0:j-1] == needle[1:i-1]
// 那么我现在就需要将后缀的开始位置向后移动了,常规是向后一位,比较 needle[0:j?] =? needle[2:i?]
// 但是这样不是浪费时间吗,我们前边做了一个前缀后缀匹配字数长度(fl数组)了,那么我们为什么还需要将后缀向后移动一位呢
// 直接从前后缀匹配的位置作为后缀的开始不好吗。也就是直接让 j = fl[j-1]
j = fl[j - 1];
}
// 比较[后缀最后位置]与[前缀最后位置]是否匹配,当然,因为之前没比较过,所以最后位置就是最开始的位置
// 如果相等,那么 j(前缀位置) 向后移动一位,,
// 之后将会比较 needle[++j] == needle[i+1],因为此时 needle[j] == needle[i],所以下一位时,这个不用比较了
if (needle.charAt(i) == needle.charAt(j)) {
++j;
}
// needle[0:i] 这个字符串前后缀匹配数量就是 0 到 j 的长度,
// 因为上边对 j 加了一,也就是 j 现在就是长度,所以直接 fl[i] = j;
fl[i] = j;
}
// 此时的i:主串的位置, j:模式串的位置
for (int i = 0, j = 0 ; i < hlen ; ++i) {
// 如果模式串当前j位置与主串i位置不相等了,那么移动模式串j到前后匹配的前缀后一位。说起来晦涩,还是看图比较明白
while (j > 0 && haystack.charAt(i) != needle.charAt(j)){
j = fl[j - 1];
}
// 如果相同,模式串j后移,之后for里边i会后移
if(haystack.charAt(i) == needle.charAt(j)) {
++j;
}
// 匹配完成
if(j == nlen) return i - nlen + 1;
}
// 都没匹配上
return -1;
}
}
算法还有改进空间,目前时间上不太充裕,先做个标记,之后来写