KMP算法
切入
去这里看:
这个算法是我去年开始看的,当时看的是韩顺平在尚硅谷https://www.bilibili.com/video/BV1E4411H73v?p=161讲的那个视频。当时我就没看懂。不过后来参考了一些其他人写的博客https://www.cnblogs.com/zzuuoo666/p/9028287.html,想了想好像懂了。如今刷力扣的时候又遇到了这个题,很离谱这题竟然标了个“简单”,真是一言难尽。
虽然说韩顺平讲的那个视频有很多关键的东西他没讲,不过他给出的这版代码属实还是很经典的。
代码
KMP算法的代码很简单,很整洁,在力扣28题里可以这样表达:
class Solution {
public int strStr(String haystack, String needle) {
if(needle.length() == 0) {
return 0;
}
//kmp,先求next数组
int[] next = new int[needle.length()];
//单个元素的字符串公共子串的长度为0
next[0] = 0;
int j = 0;
for(int i = 1; i < needle.length(); i++) {
while(j > 0 && needle.charAt(i) != needle.charAt(j)) {
j = next[--j];
}
if(needle.charAt(i) == needle.charAt(j)) {
j++;
}
next[i] = j;
}
//利用部分匹配表去匹配
j = 0;
for(int i = 0; i < haystack.length(); i++) {
while(j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = next[j - 1];
}
if(haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if(j == needle.length()) {
return i - j + 1;
}
}
return -1;
}
}
首先可以看到这代码两个for循环,里面其实一直在重复一些东西。之所以是这种简单的流程才正让人看不懂。写的太简单的东西就是这样。这里确实是有点大道至简的味道。
思想
核心思想
子串匹配母串的时候,当最终匹配失败的时候,暴力匹配的方法是,将子串向右移动一位,然后继续从两个串的头开始比较。这种方法有所冗余,因为当匹配失败的时候,子串前面有一些字符其实是匹配成功了的。如果你粗暴的往后只移动一位,那无疑是忽略了很多前面子串匹配成功所包含的信息,比如说:
- BBC ABCDABABCDABCDABDE
- ABCDABD
可以看到当子串和母串匹配了6个字符以后,子串第七个字符和母串匹配不上了。在这种情况下,虽然A和D没有成功匹配,但是子串ABCDABD的2到4位,即BCD,都曾经匹配成功过,而且这三个是不可能和子串第一位,即A,匹配成功的。所以此时需要直接右移4位,即变成:
- BBC ABCDABABCDABCDABDE
- ABCDABD
但是这里其实移动4位之后的AB也是不用比较的,直接从C开始比较,也就是i指针并不用回退,改变j指针就好。
为什么是移动4位呢?
这里需要参考另一个概念,即最大前缀后缀公共长度:
- 当字符串只包含a这个字符时,认为长度为0
- 当字符串是ab时,前缀为a,后缀为b,长度为0
- 当字符串是aba时,前缀为a、ab,后缀为a、ba,长度为0
- 当字符串是abcab时,前缀为a、ab、abc、abca,后缀为b、ab、cab、bcab,长度为2
于是在这种规则下,子串ABCDABD的前6位,即ABCDAB,的最大前缀后缀公共长度就是2。第7位匹配不上的时候,需要移动的位数就是\(6-2=4\)。这种情况下,实际上是直接跳到了下一个相似的模式之中,因为在母串中,这些匹配成功且不是最大公共前缀和后缀的字母,是不可能和子串的开头匹配成功的,所以子串可以移动直接跳过它们。例如:
- BBC ABADABABCDABCDABDE
- ABADABD
中间的AD是不可能和开头的AB成功匹配的,就直接跳过它们。假设中间这些字母能成功和子串开头匹配,那长度就不可能为现在的2。
如何证明?
反证法

用上图这种“形象”的方法,如果把上面和下面抽象位正在匹配的母串和子串。现在假设黑色的部分是最长公共前缀后缀,那匹配不成功时只需要移动到子串的头和母串的尾黑色区域重合。但是现在假设黑色部分和红色部分可以匹配,正如上图所示,那势必要求方块部分和圆圈部分是可以匹配的,那么这样最长公共前缀后缀就绝不是上图的黑色部分了,与假设产生了矛盾。
代码中的疑点
next数组的构建
首先,代码需要构建出一个next数组,这个数组的长度就是子串的长度,数组中每一个位置填写的数字,就是当前部分子串(以当前下标为尾)的最大前缀后缀公共长度。于是数组的第一个位置就是0,代表单个字符形成的字符串最大前缀后缀公共长度是0。
int[] next = new int[needle.length()];
//单个元素的字符串公共子串的长度为0
next[0] = 0;
int j = 0;
for(int i = 1; i < needle.length(); i++) {
while(j > 0 && needle.charAt(i) != needle.charAt(j)) {
j = next[--j];
}
if(needle.charAt(i) == needle.charAt(j)) {
j++;
}
next[i] = j;
}
遍历这个子串的时候,去填充这个数组。从数组的第2个位置开始填充。
while(j > 0 && needle.charAt(i) != needle.charAt(j)) {
j = next[--j];
}
这句是什么意思?
可以假设有ABCDEF...XYZABCDF这样一个子串,自然next数组的前26个位置都是0,第27个位置由于来到了A,所以填1(A),28个位置填2(AB),29个位置填3(ABC),30个位置填4(ABCD),31个位置填什么呢?自然是0。因为next[30] != next[3],所以j = next[2],即0。这里看出j = next[--j]其实是一个递归过程:

如上图所示,是一个正在设置next数组的子串,当你想要找到子串末尾E对应的数字时,发现E和j对应的字符F(在标号2右上侧)不一致,这时next[- -j]就定位到了E字符(在标号1的右上侧),这时你发现E和E是相等的字符,就找到了这个长度,就是\(4 + 1 = 5\)了。你会发现2与3是可以匹配的,而2与1也是可以匹配的,这样1与3就是可以匹配的了。
这样,next数组就构建出来了。
匹配
匹配的过程代码和构建数组时,是极其类似的。
//利用部分匹配表去匹配
j = 0;
for(int i = 0; i < haystack.length(); i++) {
while(j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = next[j - 1];
}
if(haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if(j == needle.length()) {
return i - j + 1;
}
}
return -1;
思路可以说和暴力匹配的时候差不多,如果母串和子串能匹配上,就i和j分别自增,看下一位。如果匹配不上,这时候子串就要移动了,移动在宏观表现为j指针的变化,j现在指向next[j - 1]了。如果不相等就递归一直往前找,这样可以保证j在减小的同时,j后面那部分模式串和i后面的模式串是可以匹配的。最终j有可能归0,即回到了子串的开头。
本文来自博客园,作者:imissinstagram,转载请注明原文链接:https://www.cnblogs.com/LostSecretGarden/p/15986033.html

浙公网安备 33010602011771号