1.参考视频
2.理论部分
如果暴力BF算法,回溯的重复度过高,算法复杂度O(m*n),应用剪枝,而KMP就是一种剪枝方法。
例如图中在匹配到此处不匹配时,看前面已经匹配过的子串的最长公共前后缀(最长相等前后缀)前后缀不可以是字符串本身
前缀(不包含整串) | 后缀(不包含整串) | 相等? |
a | b | × |
ab | ab | √ |
abb | bab | × |
abba | bbab | × |
所以最长公共前后缀是ab,所以第一个a移动到下一个a位置。
而且子串的公共前后缀看其本身就ok,完全不需要去看待匹配的串。所以引入了next数组的概念。
然后对于KMP算法来说时间复杂度为O(n),n为主串的长度
匹配时通过next数组来决定,移给当前指针的下标是子串中哪个下标
例如,A和C此处无法匹配,而前一个next数组值为2,(ABAB的最长相等前后缀为AB,长度为2),所以下标2的A移到当前指针下面去
扫描指针只可前进不可回溯。
3.递推求解next数组
---->有的解析说next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1
---->有的解析说next数组相当于“最大长度值”,初始值为0
.......
其实就是一个怎么移的区别。
反正i(主串里面)一直与j(子串里面)匹配,i永远向前,j在往前跳
比如这里的子串ABABC
A | B | A | B | C |
-1 | 0 | 0 | 1 | 2 |
(默认都是-1) | A的最长公共前后缀长度 | i-1处与前缀处不相等 | i-1处与前缀处相等 | i-1处与前缀处相等 |
前缀处指的是前一个next数组里面的值所对应的下标处。next数组里面是最长公共前后缀的长度
对应的下标就已经不含有公共前后缀了,是公共前后缀后面一个数。下标索引从0开始。
next数组的递推求解---->i处的next数组的值
1.char2[i-1]的值去跟char2[next[i-1]]比较,(之前提到的前缀处)
2.相等的话,next[i]=next[i-1]++;
3.不相等的话,char2[i-1]去跟char2[next[i-1]]的前缀处比较,依次类推
就是说,一直往前去比只要这边的next数组还有值就往前去找
4.KMP过程
在已知next数组的前提下,i1遍历主串,i2遍历子串,i2可以向前回溯。
1.i1和i2对应的字符相等,i1++;i2++;
2.不相等并且next[i2]=-1;就是子串第一个字符,往前无可前的时候,i1++;
3.不相等的其他情况,i2往next[i2]处跳,i2=next[i2]
最后对比i2走到最后的话,返回i1-i2(第一个出现的位置)
5.KMP完整代码
class Solution {
/**
* KMP算法,在haystack里面找出needle是否出现,返回出现的下标
*
* @param haystack
* @param needle
* @return
*/
public int strStr(String haystack, String needle) {
int len1 = haystack.length(), len2 = needle.length();
if (len1 < len2) {
return -1;
}
char[] ch1 = haystack.toCharArray();
char[] ch2 = needle.toCharArray();
int[] next = getNext(ch2);
int i = 0, j = 0;
while (i < len1 && j < len2) {
if (ch1[i] == ch2[j]) {
i++;
j++;
} else if (next[j] == -1) {
i++;
} else {
j = next[j];
}
}
return j == len2 ? i - j : -1;
}
public int[] getNext(char[] ch) {
if (ch.length == 1) {
return new int[]{-1};
}
int[] next = new int[ch.length];
next[0] = -1;
next[1] = 0;
int i = 2;
int cn = 0;
while (i < ch.length) {
if (ch[i - 1] == ch[cn]) {
next[i++] = ++cn;
} else if (cn > 0) {//不相等的话并且cn有值
cn = next[cn];
} else {
next[i++] = 0;//cn=0或者为-1
}
}
return next;
}
}
6.总结
KMP还是磨了蛮久的,其实说实话写到现在说很懂也不能算,不过大概过程分为两部分,next数组求解,和整体求解。
此间有最长公共前后缀来构建next数组,而next数组的解法可以动态规划优化,并且代码里面我采用的是cn标注去参考的前缀处地方,有的参考代码直接省去了这个步骤直接用的j。
包括整体求解的时候,i不动,j往前动,动到next数组的下标。
之后复习的时候还得再多轮几遍
2022.10.08补