解析:(KMP算法)
题目要求:给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
大体思路:这道题是典型的KMP算法题。需要寻找最长公共前后缀来做。
KMP算法:
作用:解决字符串匹配问题。
核心思路:当遇到不匹配的情况时,寻找不匹配位置的前一位的子串的最长相等前后缀,找到后,从相等的前缀位置后一位继续匹配。
原理:相比于暴力法,KMP关注已经匹配过的字符串,若出现了不匹配,如何充分利用已经匹配的部分。
为什么要找相等的前缀和后缀?因为可以把匹配过的后缀当成即将重新匹配的前缀,这样就不需要再匹配与后缀相等的前缀了,节省了开销。
为什么不像暴力法一样,如果当前匹配失败,从文本串开头的后一位开始匹配,而是从文本串中不匹配的字符开始?因为子串一旦满足前缀和后缀相等,子串相等的前后缀可以看做是关于中心对称的,能匹配的只能是两端对称的部分,其他部分不能匹配。这样做,很大地节省了开销。
&[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yKXyDBMY-1679492028860)(C:\Users\张凯旗\Pictures\Camera Roll\KMP精讲1.gif)]
制作next数组:
实现方法:
(1)制作next数组:先对模式串 needle 的所有子串找出最长相等子序列,按照next下标位置放入next数组中。
①初始化:
定义一个next数组,用于记录模式串的所有子串的最长相等前后缀长度,next的下标即为子串在模式串中的位置。
定义i记录最长相等前后缀的后缀末尾,j记录最长相等前后缀的前缀末尾(j同时也记录最长相等前后缀的长度)。
i初始值为1,j初始值为0.(为什么i初始值为1,j和i不指向同一位置,这样才可以相互比较,i又是指代后缀,所以i在j后面。如果i初始化为0就是死循环)
②从0到末尾for循环i。每次循环考虑两种情况:
情况一、j对应的值和i对应的值不相等,j向前回退,一直回退(所以是while循环)到和i对应的值相等,或者一直回退到0位置。
情况二、j对应的值和i对应的值相等,j++。
经过①和②就得到了next表
(2) 寻找符合要求的下标:
循环遍历haystack字符串
如果和needle模式串不匹配,while回退j到next[j-1],也就是将当前后缀回退前一位的在next数组中对应的位置。一直到匹配或者j到开头的数组。
如果和needle模式串匹配,i和j都一起向后迭代。指导j == 模式串的length(),说明找到了这个值。返回i - 模式串的length() - 1;
注意点:(if和while上下位置不能颠倒)
① next数组初始化时,表示后缀的i需要初始化为1,不然陷入死循环。
② 在(2)寻找符合要求的下标中,while循环结束了,还要执行一下if,因为while负责使得j向前回退,如果不是因为j==0结束了循环,还要把j对应的值和i对应的值比较一下,也就是执行下面的if。
遗留问题:(制作next数组中,为什么回退不可以是j–? 为什么一定要是j = next[j - 1];)
代码:
class Solution {
public int strStr(String haystack, String needle) {
if (haystack.length() < needle.length()) return -1;
int[] next = new int[needle.length()];
getNext(next, needle);
// j表示模式串needle的下标
int j = 0;
// i表示haystack发下标
for (int i = 0; i < haystack.length(); i++) {
// 如果不相等,跳转到j前一位next对应的位置,也就是跳转到j前一位的最长相等前缀的最后一位
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = next[j - 1];
}
// 当两个字符相等,或者不相等但是经过上面的while跳转到j新的位置使得两个字符相等
// 这时候j和i一起向后迭代
if (haystack.charAt(i) == needle.charAt(j)) {
j++;
if (j == needle.length()) return i - needle.length() + 1;
}
}
return -1;
}
public void getNext(int[] next, String s) {
// 初始化前缀为0
int j = 0;
// 后缀i从1开始遍历
for (int i = 1; i < s.length(); i++) {
// 当后缀和前缀对应的字符不同时,前缀向前回退
//(注意:回退到j前一个位置对应的next数组的值对应的位置)
// 为什么不可以是j--?
while (j > 0 && s.charAt(i) != s.charAt(j)) j = next[j - 1];
// 如果后缀和前缀对应的字符相同,则前后缀一起向后迭代
//(注意:上一步的while执行完了会进行这个if判断,也就是如果回退到和i对应字符相等,j还要++)
if (s.charAt(i) == s.charAt(j)) j++;
next[i] = j;
}
}
}
注释:图片来自于代码随想录