KMP
KMP算法解决的问题
有两个字符串str1和str2,str1 是否包含str2,如果包含返回str2在str1中开始的位置。如何做到时间复杂度0(N)完成?
经典算法就是遍历str1的每个字符,以该字符为头去匹配str2,假如str1的长度为n,str2的长度为m,则时间复杂度为 O ( n m ) O(nm) O(nm)
最大前缀和
这个概念是用于要匹配的字符串,就是上面的str2。举例:str2=abcgcbaY
,对于Y,来计算其最大前缀和c:
前缀 | 后缀 | 是否相等 | |
---|---|---|---|
c=1 | a | a | ✅ |
c=2 | ab | ba | ❌ |
c=3 | abc | cba | ❌ |
c=4 | abcg | gcba | ❌ |
c=5 | abcgc | cgcba | ❌ |
c=6 | abcgcb | bcgcba | ❌ |
假如Y前面有n个字符,那么c不会取到n来比较的,因为必然相等,所以没意义。由上表可知,Y的最大前缀和为1。针对需要匹配的字符串会开辟一个等长的next数组,用于存放对应字符的最大前缀和。
- next[0] 规定为 − 1 -1 −1 因为str2中0位字符前面没有东西
- next[1]规定为 0 0 0 str2中1位字符前面只有1个字符,但是在计算最大前缀和时又不能取到整个前面字符串的长度,所以规定为0
str2=bgeddfbgeY
此时Y的最大前缀和为
3
3
3
算法流程
假设str1和str2从
i
i
i 一直到
x
−
1
x-1
x−1 都和str2对应相等,但是
s
t
r
1
[
x
]
≠
s
t
r
2
[
y
]
str1[x] \neq str2[y]
str1[x]=str2[y] ,只有最后一个匹配不上。如果是经典方法,就会是i-->i+1 j-->0
然后继续匹配。KMP算法 总体路线也是这样,但是会利用最大前缀和加速,去除重复匹配。
n e x t [ y ] = c next[y]=c next[y]=c 表示y的最大前缀和为c,所以 s t r 2 [ 0 ] ~ s t r 2 [ c − 1 ] = s t r 2 [ y − c ] ~ s t r 2 [ y − 1 ] str2[0]~str2[c-1] = str2[y-c]~str2[y-1] str2[0]~str2[c−1]=str2[y−c]~str2[y−1]
又因为 s t r 1 [ i + y − c ] ~ s t r 1 [ x − 1 ] = s t r 2 [ y − c ] ~ s t r 2 [ y − 1 ] str1[i+y-c]~str1[x-1]=str2[y-c]~str2[y-1] str1[i+y−c]~str1[x−1]=str2[y−c]~str2[y−1]
所以 s t r 1 [ i y − c ] ~ s t r 1 [ x − 1 ] = s t r 2 [ 0 ] ~ s t r 2 [ c − 1 ] str1[i_y-c]~str1[x-1]=str2[0]~str2[c-1] str1[iy−c]~str1[x−1]=str2[0]~str2[c−1]
所以只需要让 i-->x j-->c
这样就省了很多匹配的步骤,达到了加速的效果
问题:如何保证在 i ~ x − 1 i~x-1 i~x−1 中没有一个元素能和str2匹配上呢?i - -> x会不会漏掉呢?
用反证法来证明不存在漏掉的情况,假设在 ( i + c − 1 ) ~ ( x − c ) (i+c-1)~(x-c) (i+c−1)~(x−c) 之间有一个 k k k,使得以 k k k 为头能和str2完全匹配上,那么就有:
s t r 1 [ k ] ~ s t r 1 [ x − 1 ] = s t r 2 [ 0 ] ~ s t r 2 [ x − 1 − k ] str1[k]~str1[x-1]=str2[0]~str2[x-1-k] str1[k]~str1[x−1]=str2[0]~str2[x−1−k]
又因为: s t r 1 [ k ] ~ s t r 1 [ x − 1 ] = s t r 2 [ k − i ] ~ s t r 2 [ x − 1 − k − i ] str1[k]~str1[x-1]=str2[k-i]~str2[x-1-k-i] str1[k]~str1[x−1]=str2[k−i]~str2[x−1−k−i] 此时的最大前缀长度超过了C,
所以矛盾,所以反证法成立。
整个匹配的过程如下图所示:
求next数组
如果next[i]>next[i-1],则next[i]=next[i-1]+1
,为什么?可以用反证法证明
假设 n e x t [ i − 1 ] = 2 , n e x t [ i ] = 4 next[i-1]=2, \;next[i]=4 next[i−1]=2,next[i]=4
由图片可以看出在 n e x t [ i ] = 4 next[i]=4 next[i]=4 的情况下, n e x t [ i − 1 ] = 3 next[i-1]=3 next[i−1]=3
算法实现
public class KMP {
// 若des是ori的子串,返回匹配成功的索引;若不存在返回-1
public static int getIndexOf(String ori, String des){
if (ori == null || des == null || ori.length() < 1 || ori.length() < des.length())
return -1;
char[] source = ori.toCharArray();
char[] template = des.toCharArray();
// 获取匹配字符串的最大前缀和数组
int[] next = getNextArr(template); // O(M)
int i = 0, j = 0;
while (i < source.length && j < template.length){ // O(N)
if (source[i] == template[j]){
i++;
j++;
} else if (next[j] == -1) { // 等价于 j == 0
i++;
} else {
j = next[j];
}
}
// i越界了或j越界了。上面while中,只有两个元素相等j才会往后移动,所以如果是j越界了就说明匹配成了
return j == template.length ? i - template.length : -1;
}
private static int[] getNextArr(char[] template) {
if (template.length == 1)
return new int[] {-1};
int[] next = new int[template.length];
next[0] = -1;
next[1] = 0;
int i = 2, cn = next[i - 1];
while (i < next.length){
if (template[cn] == template[i - 1])
next[i++] = ++cn;
else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0;
}
}
return next;
}
}