1 问题描述
在进行字符串操作的时候,我们有一个需求,从给定的一个主串
s
=
s
0
s
1
.
.
.
s
m
−
1
s=s_0s_1...s_{m-1}
s=s0s1...sm−1中查找是否包含了某个模式串
t
=
t
0
t
1
.
.
.
t
n
−
1
t=t_0t_1...t_{n-1}
t=t0t1...tn−1,并返回模式串在主串中首次出现时的第一个字符在主串中的位置。用接口描述为 int indexOf(String str, int start);
调用时,int index = subStr.indexOf(str, start);
这样就可以返回subStr子串在str中的起始位置了。
2 理论分析
最简单的实现方法就是暴力搜索Brute-Force:将模式串作为滑动窗口在主串上每滑动一格,然后将主串对应的字符依次与模式串比较。遇到第一个不匹配就将窗口向后滑动一格,模式串指针回退到0,主串指针与模式串对应,又依次比较,直到找到与模式串完全匹配的位置返回,若没有找到返回-1。这种方法实现简单,缺点是时间复杂度较高,为 O ( m × n ) O(m\times n) O(m×n),而KMP算法的时间复杂度仅为 O ( m + n ) O(m + n) O(m+n)。
KMP算法核心思想:当某次匹配失败( s i s_i si /= t j t_j tj)时,主串 s s s的当前比较位置 i i i不必回退,此时主串中的 s i s_i si可直接和模式串某个 t k ( 0 < k < j ) t_k(0<k<j) tk(0<k<j)进行比较,此处下标 k k k的确定与主串无关,只与模式串本身的构成有关,即从模式串本身就可计算出 k k k的值。
理论分析:设主串
s
=
s
0
s
1
.
.
.
s
m
−
1
s=s_0s_1...s_{m-1}
s=s0s1...sm−1,模式串
t
=
t
0
t
1
.
.
.
t
n
−
1
t=t_0t_1...t_{n-1}
t=t0t1...tn−1,从主串的某个位置开始比较,当匹配不成功(
s
i
s_i
si /=
t
j
t_j
tj)时,在这前面的一段一定是匹配的,即
s
i
−
j
s
i
−
j
+
1
.
.
.
s
i
−
1
=
t
0
t
1
.
.
.
t
j
−
1
s_{i-j}s_{i-j+1}...s_{i-1} = t_0t_1...t_{j-1}
si−jsi−j+1...si−1=t0t1...tj−1
- 若模式串中不存在任何满足式
t 0 t 1 . . . t k − 1 = t j − k t j − k + 1 . . . t j − 1 ( 0 < k < j ) ( ∗ ) t_0t_1...t_{k-1} = t_{j-k}t_{j-k+1}...t_{j-1}\ \ (0<k<j) \ \ (*) t0t1...tk−1=tj−ktj−k+1...tj−1 (0<k<j) (∗)
则说明在模式串 t 0 t 1 . . . t j − 1 t_0t_1...t_{j-1} t0t1...tj−1中不存在前缀子串 t 0 t 1 . . . t k − 1 ( 0 < k < j ) t_0t_1...t_{k-1}\ \ (0<k<j) t0t1...tk−1 (0<k<j)与主串 s i − j s i − j + 1 . . . s i − 1 s_{i-j}s_{i-j+1}...s_{i-1} si−jsi−j+1...si−1中的 s j − k s j − k + 1 . . . s j − 1 s_{j-k}s_{j-k+1}...s_{j-1} sj−ksj−k+1...sj−1子串相匹配,下一次可直接比较 s i s_i si和 t 0 t_0 t0。 - 若模式串中存在满足 ( ∗ ) (*) (∗)式的子串,则说明模式串 t 0 t 1 . . . t n − 1 t_0t_1...t_{n-1} t0t1...tn−1中的前缀子串已经与主串 s i − j s i − j + 1 . . . s i − 1 s_{i-j}s_{i-j+1}...s_{i-1} si−jsi−j+1...si−1中的 s j − k s j − k + 1 . . . s j − 1 s_{j-k}s_{j-k+1}...s_{j-1} sj−ksj−k+1...sj−1子串相匹配,下一次可直接比较 s i s_i si和 t k t_k tk。
理解:KMP算法关键是确定比较失败之后子串需要向后滑动的步数k。k值求法是看模式串中匹配失败点j前面的
t
0
t
1
.
.
.
t
j
−
1
t_0t_1...t_{j-1}
t0t1...tj−1子串中,前几个字符,与后几个字符匹配的最大长度即为
k
k
k。
例如:
t
0
t
1
.
.
.
t
j
−
1
t_0t_1...t_{j-1}
t0t1...tj−1 = abcab,k = 2;
t
0
t
1
.
.
.
t
j
−
1
t_0t_1...t_{j-1}
t0t1...tj−1 = ababa,k = 3。
求模式串的k值数组:模式串的每一个 t j t_j tj都有一个 k k k值对应,这个 k k k值仅与模式串本身有关,而与主串 s s s无关。因此我们先将每一 t j t_j tj对应的位置的k值求出来,得到一个数组,这样每当我们与匹配失败时,就可以根据索引找到对应的k值。
3 算法实现
3.1 求next[j]
/**
* 辅助函数:字符串匹配函数中求next[j]
* @param T IString是自己实现的String类,可以当做String用
* @return 返回模式串的所有k值数组
*/
private int[] getNext(IString T) {
int[] nextval = new int[T.length()];
int j = 0;
int k = -1;
nextval[0] = -1;
while (j < T.length() - 1) {
if (k == -1 || T.charAt(j) == T.charAt(k)) {
j++;
k++;
if (T.charAt(j) != T.charAt(k)) {
nextval[j] = k;
} else {
nextval[j] = nextval[k];
}
} else {
k = nextval[k];
}
}
return nextval;
}
/**
* KMP算法
* @param T 主串
* @param start 从模式串的start位置开始向后搜索
*/
@Override
public int indexOf(IString T, int start) {
int[] next = getNext(T);
int i = start;
int j = 0;
while (i < this.length() && j < T.length()) {
if (j == -1 || this.charAt(i) == T.charAt(j)) {
i++;
j++;
} else {
j = next[j];
}
}
if (j < T.length()) {
return -1;
} else {
return (i - T.length());
}
}