简介
用于子字符串查找
首先是暴力查找
//主串 子串
public static int search(String text, String pattern) {
int N = text.length();
int M = pattern.length();
for (int i = 0; i < N-M; i++) {
int j;
for (j = 0; j < M; j++) {
if (text.charAt(i+j) != pattern.charAt(j))
break;
}
if (j == M) return i;
}
return -1;
}
最坏时间复杂度为O(N*M)
KMP算法思想
暴力查找之所以慢是因为它每次的匹配都是从头开始,并且抛弃了之前已经算好的结果
KMP利用被匹配字符串的相同前缀和后缀,来减少回溯成本。
如下图
当匹配到i=4,j=4时,text的“a"不等于pattern的"c",在暴力算法中,会把j置零,重新开始比较。而KMP算法先不急把j置零,先尝试在之前匹配过的字符串中查找相同的无重叠的最大前缀子字符串和最大后缀子字符串。如“abab“,相同的无重叠的最大前缀子字符串和最大后缀子字符串为”ab“,记住,是“无重叠的”,字符串“ababa”的最大前缀子字符串和最大后缀子字符串是“a”而不是“aba”。
因为之前模式字符串的”abab“已经跟文本字符串匹配,所以文本字符串也存在”abab“,那么我们显然不需要再从头计算,把相同的最大前缀子字符串对齐最大后缀子字符串即可。如图所示。
我们可以在匹配前先算好模式字符串中从0到m(m在区间[0, M]内, M为模式字符串的长度)各个子字符串中的相同的无重叠的最大前缀子字符串和最大后缀子字符串,从而在匹配过程中直接使用,减少计算次数。
如字符串 “ababcab” 的从0到m各个子字符串中的相同的无重叠的最大前缀子字符串和最大后缀子字符串的长度k和索引j的关系
a | ab | aba | abab | ababc | ababca | ababcab | |
---|---|---|---|---|---|---|---|
k | 0 | 0 | 1 | 2 | 0 | 1 | 2 |
j | -1 | -1 | 0 | 1 | -1 | 0 | 1 |
从上表的,j=k-1,因为在程序语言中,字符串的索引是从0开始的,所以减1,而j=-1,表示不存在相同的无重叠的最大前缀子字符串和最大后缀子字符串。最后用到的是j的值
计算模式字符串的next数组
/**
* 计算模式字符串的next数组
* @param pattern 模式字符串
* @return next数组,next数组的值对应模式字符串中从0到m各个子字符串中的相同的无重叠的最大前缀子字符串和最大后缀子字符串
*/
private static int[] calNext(final String pattern) {
int M = pattern.length();
int[] next = new int[M];
next[0] = -1; // 第一个子字符串只有一个字符,肯定不存在相同前后缀子字符串
int k = -1; // k代表是相同的无重叠的最大前缀子字符串和最大后缀子字符串的长度减1,为-1表示不存在相同子串
for (int i = 1; i < M; i++) {
// 这里k也充当了低位索引,i是高位索引
while(k > -1 && pattern.charAt(k+1) != pattern.charAt(i)) {
k = next[k]; // 字符不相等,k需要回溯
}
if (pattern.charAt(k+1) == pattern.charAt(i)) {
k++;
}
next[i] = k;
}
return next;
}
比较难理解的是最里面的循环while
k>-1
,表示子串pattern[0, k]
中已存在相同的无重叠的最大前缀子字符串和最大后缀子字符串,如果此时低位字符k+1
跟高位字符i
不匹配,k就需要回溯,把指针回退到模式字符串子串[0, k]
中的相同的无重叠的最大前缀子字符串和最大后缀子字符串的长度,而这个值之前已经计算过,就是next[k]
。
KMP主函数
public static int search(final String text, final String pattern) {
int[] next = calNext(pattern);
int k = -1;
int N = text.length();
int M = pattern.length();
for (int i = 0; i < N; i++) {
while(k > -1 && pattern.charAt(k+1) != text.charAt(i)) {
// 不匹配回溯找最大相同前后缀子字符串
k = next[k];
// 回溯方式讲解:
// 假设:
// text: ...abac...
// pattern: abad...
// i指向c时,k指向第二个a,此时k+1指向d不等于c,那么需要回溯
// 此时我们需要找的是 text[i-1-k, i-1] 子串中最大相同前后缀子字符串,
// 因为 pattern[0, k] == text[i-1-k, i-1]
// 而 pattern[0, k] 的最大相同前后子字符串之前已经算过了,是next[k]
}
if (pattern.charAt(k+1) == text.charAt(i))
k++;
if (k == M - 1)
return i - M + 1; // 已找到匹配字符
}
return -1; // 未找到匹配字符
}
KMP算法的时间复杂度为O(N+M)
参考文章
https://www.jianshu.com/p/f65cae7e00ef