KMP
KMP算法解决的问题
有两个字符串str1和str2,str1 是否包含str2,如果包含返回str2在str1中开始的位置。如何做到时间复杂度0(N)完成?
经典算法就是遍历str1的每个字符,以该字符为头去匹配str2,假如str1的长度为n,str2的长度为m,则时间复杂度为O(nm)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]规定为000 str2中1位字符前面只有1个字符,但是在计算最大前缀和时又不能取到整个前面字符串的长度,所以规定为0
str2=bgeddfbgeY
此时Y的最大前缀和为333
算法流程
假设str1和str2从iii 一直到x−1x-1x−1 都和str2对应相等,但是str1[x]≠str2[y]str1[x] \neq str2[y]str1[x]=str2[y] ,只有最后一个匹配不上。如果是经典方法,就会是i-->i+1 j-->0
然后继续匹配。KMP算法 总体路线也是这样,但是会利用最大前缀和加速,去除重复匹配。
next[y]=cnext[y]=cnext[y]=c 表示y的最大前缀和为c,所以 str2[0]~str2[c−1]=str2[y−c]~str2[y−1]str2[0]~str2[c-1] = str2[y-c]~str2[y-1]str2[0]~str2[c−1]=str2[y−c]~str2[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]str1[i+y−c]~str1[x−1]=str2[y−c]~str2[y−1]
所以 str1[iy−c]~str1[x−1]=str2[0]~str2[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−1i~x-1i~x−1 中没有一个元素能和str2匹配上呢?i - -> x会不会漏掉呢?
用反证法来证明不存在漏掉的情况,假设在(i+c−1)~(x−c)(i+c-1)~(x-c)(i+c−1)~(x−c) 之间有一个 kkk,使得以kkk 为头能和str2完全匹配上,那么就有:
str1[k]~str1[x−1]=str2[0]~str2[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]
又因为: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]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
,为什么?可以用反证法证明
假设next[i−1]=2, next[i]=4next[i-1]=2, \;next[i]=4next[i−1]=2,next[i]=4
由图片可以看出在next[i]=4next[i]=4next[i]=4 的情况下,next[i−1]=3next[i-1]=3next[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;
}
}