给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
题解:暴力解法就不讲了。讲讲kmp算法。
先看kmp为什么能减小时间复杂度及其正确性证明,设文本串haystack为aabaaac,模块串needle为aac。n = haystack.length,m = needle.length
令指针i = 0指向haystack,j = 0 指向needle。假如是暴力解法,当i = 2,j = 2时字符不相等,指针i会回溯到1 的位置,j回溯到0重新开始。
为了减少时间复杂度,我们就会想,假如j回溯至0而i不回溯呢。显然时间复杂度会从o(mn)减小到o(n)。但是这样会导致一个问题,当i = 5,j = 2时a!=c,不匹配,此时若是回溯j = 0而i不变,得到haystack子串ac不匹配needle,return -1。显然与答案不符。
这样做的根本问题是在回溯j的时候会错过已经匹配了的haystack字串(本例中是一个a)。
显然,我们如果将j不回溯到0,而是回溯到1,就会匹配成功了。
同理,在每一个不匹配的状态下,j都会回到一个特定的位置。而这位置与haystack和i无关,只与needle有关。
这种无后效性的递推方式,其实就是动态规划的思想。每一个不匹配时的j,都是一个状态。而kmp的next[i]其实就是dp[i]。
那么怎么得到next[i]呢?
这里要引入前后缀和最长相等前后缀的知识。对于字符串aac来说,不看最后一个字符,并且以第一个字符开始的子串及aac的前缀,即a,aa。同理后缀为c,ac。此时我们知道aac没有相等的前后缀,next[2]=0。同理对于aa字串,其最长相等前后缀为a,所以next[1]=1。而a字串没有前后缀,next[0]=0。
j | 0 | 1 | 2 |
子串 | a | aa | aac |
next | 0 | 1 | 0 |
为什么求最长相等前后缀就可以得到next数组呢?回到前面的例子。回溯j=0不行的原因其实是由于aac串子串aa中存在相等的前后缀a,回溯j=0会使我们漏掉文本串中已经匹配的a,此时我们将移动至1(即最长相等前后缀中前缀的下一个位置)就能使已经匹配的信息继续保持匹配。
接下来就是怎么实现求next[i]的问题了。这里结合代码进行讲解。
public void get_next(int[] next, String needle){
int j = 0;
for(int i = 1; i < needle.length(); ++i){
while(j > 0 && needle.charAt(i) != needle.charAt(j)){
j = next[j - 1];//2.这里是最难理解的
}
if(needle.charAt(i) == needle.charAt(j)){
j++;//1.
}
next[i] = j;
}
}
这里的i相当于上方表格中的j,needle[0-i]也就代表了模块串的每一个子串。而这里的j代表当前字符串最长相等前后缀长度(也是其前缀下标+1)。
从i=1开始,当needle[i]==needle[j]时我们就让j++,并且i++。显然,若是一直相等下去,那么两个指针一直同时移动,随着i++,next也就++,j则一直标记着最长相等前后缀长度(也是其前缀下标+1)的位置。也就是1.处代码的含义。
那么当needle[i]==needle[j],显然我们要将j回退到一个地方,那么回退到哪里呢。这里有一点动态规划的思想,利用先前求好的状态来求当前状态。也是实现部分最难理解最抽象的地方。即j = next[j-1]这行代码。
设模式串
j1 | j0 | i | ||||||||
a | c | a | a | b | ... | a | c | a | a | c |
当i,j处于此处时模式串失配。令j回溯至某个位置并且i不变。听起来是不是有点耳熟。是的,这其实就是之前文本串和模式串匹配时我们的思路。那时的文本串就是此时的模式串,而模式串就是字串abaab。按照之前的想法,我们应该让j=next[j-1]来避免错过已匹配字符。不断执行这条语句直到needle[i]==needle[j],此时的j1就是相等前缀的位置了。
当我们得到了next[i]之后,接下来的事就简单了。在此不赘述。
代码:
class Solution {
public int strStr(String haystack, String needle) {
int[] next = new int[needle.length()];
get_next(next, needle);
for(int i = 0, j = 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 - needle.length() + 1;
}
}
return -1;
}
public void get_next(int[] next, String needle){
int j = 0;
for(int i = 1; i < needle.length(); ++i){
while(j > 0 && needle.charAt(i) != needle.charAt(j)){
j = next[j - 1];
}
if(needle.charAt(i) == needle.charAt(j)){
j++;
}
next[i] = j;
}
}
}