昨天天杀的 做了个字符串匹配的题,于是天杀的 重新学习 kmp算法(实际上第一次学的时候就没搞懂 )。
首先搞清楚几个概念:①什么是字符串匹配?比如给定字符串"aabcaa",想要知道"abca"是否存在于该字符串中,这个过程就是字符串匹配。类似于String.indexOf()。②什么是kmp算法?如果暴力破解,“abca”在匹配的过程中,先会匹配a,然后是b,如不同,就将"aabcaa"的指针向后移一位,再重新匹配。kmp使用next数组,将重新匹配的过程减少,使得效率大大提升。next数组只与“abca”有关。
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
示例 1:
输入: “abab”
输出: True
解释: 可由子字符串 “ab” 重复两次构成。 示例 2:
输入: “aba”
输出: False 示例 3:
输入: “abcabcabcabc”
输出: True
解释: 可由子字符串 “abc” 重复四次构成。 (或者子字符串 “abcabc” 重复两次构成。)
先放代码:
class Solution {
public boolean repeatedSubstringPattern(String s) {
return kmp(s + s, s, 1) != s.length();
}
private int kmp(String s1, String s2, int p) {
char[] s = s1.toCharArray();
char[] t = s2.toCharArray();
int n1 = s.length, n2 = t.length;
int[] next = getNext(t);
//i是s当前需要比较的字符,j是t当前比较的字符,也是前后缀相同字符的个数
int i = 0, j = 0;
while (i < n1 && j <= n2) {
if (j == n2) {
if (p == 0) break;
else {
p--;
j = next[j];
continue;
}
}
if (j == -1 || s[i] == t[j]) {
i++;
j++;
} else {
j = next[j];
}
}
if (j == n2) {
return i - j;
}
return -1;
}
private int[] getNext(char[] t) {
int[] next = new int[t.length + 1];
//k的含义是:前j个字符中,有k个是前后缀相同的。
int j = 1, k = 0;
next[0] = -1;
while (j < t.length) {
if (k == -1 || t[k] == t[j]) {
next[++j] = ++k;
} else {
//此时左边k的含义是:前k个字符中,有next[k]个是前后缀相同的。
//取得的k的可以用在t[k] == t[j]时,表示前j个字符中,有k + 1(1就是第j位)个前后缀相同
k = next[k];
}
}
return next;
}
}
最关键的还是next数组,次关键的是next数组的应用。
next数组
next数组的值是最长公共前后缀,在第 j 的位置(j 是长度),有k个字符(即前缀)与后k个字符(即后缀)相同。
比如下图 next[6] = 2 是因为前6个字符中的前缀(前两个"abxxxx" )与后缀(后两个 “xxxxab” )相同。
测试一下理解:上图中,不看已给结果,算一下next[8]是多少?
next数组如何生成?
怎么编码呢?
next数组的计算,以这张图辅以理解,想象出一个双指针 j 和 k ,j 是第 j 个字符,用在t[j] 和 next[++j] (t数组声明在代码),表示着当前正在比较的字符。k 是前后缀相同字符的个数,k 总是小于 j 的。每次 t [ k ] 与 t [ j ] 进行比较时,如果相同,那就让 next [ ++j ] = ++k (因为next是长度为下标所以要+1) 。如果不相同,就让 k = next[k] ,这个的含义在代码中写了,它的目的是减少重复比较,直接得出前后缀有多少个是相同的。
那这个next[0] = -1 的含义呢?其实他是不是-1根本无所谓,只要是负数就行,但不能是非负数因为会和next数组的其他项重复。-1就是一个标志,他标志着在 j 这个位置的所有前面的字符都没有前后缀,也标志着第0位与第 j 位不相等。
为什么设置这个标志位?先看为什么 k 会等于-1,k会等于-1就是上一次 k = 0 ,这又是因为上一次第 j 位与第 0 位不相等,既然已经不相等了,那就需要让 j++,让next[j] = 0,让 k = 0。设置了这个标志位,就是设置了循环终止符,循环是k = next[k];
所产生的循环。如果不设置这个标志位next[0],就不能确定这次是比较还是让 j++。
用上图给出的字符串在脑海中测试一下,看看自己理解的k = next[k];
有没有问题,如果有问题,你会在next[8]处写上0(我之前就是 )
next数组的应用
现在有主串S:ababcabcacbab,模式串T:abcac。i从0开始,j也从0开始。
根据上述方法可以知道,T的中每个字符对应的失效函数值为:
(将表中公共前后缀最长长度全部右移一位,第一个值赋为-1)
第一次匹配中,i从0开始,j从0开始。当i=2,j=2时匹配失败,此时i不动,next[j]=next[2]=0,接下来模式串T要相对于主串S向右移动j - next [j] = 2 位,j回溯到0。
第二次匹配中,i从2开始,j从0开始。当i=6,j=4时匹配失败,此时i不动,next[j]=next[4]=1,接下来模式串T要相对于主串S向右移动j - next [j] = 3位,j回溯到1。
第三次匹配中,i从6开始,j从1开始。当i=10,j=5时匹配成功,返回i - j = 5。