模式匹配
从字符串(s)中查找某字符串(t)出现的位置,如果多次出现,则返回最小下标。比如 s="abaacdeacd",t="acd",查找的结果就是 3
暴力解法
这个方法思路简单。就是让 t 依次从 s 的每一个字符串开始比较,看有没有匹配的。最差时间复杂度为 O(nm),其中 n=len(s), m=len(t)
public static int indexOfStr(String s, String t) {
for(int i = 0; i < s.length() - t.length(); i++) {
int j = 0;
for(; j < t.length(); j++) {
if(s.charAt(i + j) != t.charAt(j))
break;
}
if(j == t.length()) return i;
}
return -1;
}
KMP算法
问题提出:暴力解法中,s 和 t 一失配,那么指向 s 和 t 的指针 i、j 都要往回移动,其中 i 自增到下一个字符位置,j 则回到 t 的起始位置。这里 j 回退到起始位置是合理的,但是 i 仅仅自增到下一字符的位置,就没那么必要了,因为会出现很多重复比较。因该利用某种机制减少这种不必要的比较。
例如下图当中,当 s 和 t 比对到那个位置的时候,发现 s[i] != t[j],这时候暴力解法就得让 i 回退到 index = 4的位置,而 j 则要回到 t 串的起始位置开始比较。
我们知道当 s[i] != t[j] 的时候,t[0,1,......, j-1]是和 s 串匹配的。如果我们知道 t[0,1,......, j-1] 的所有前缀和后缀子串中的最长匹配串的长度为 k,也就是 t[0, 1, ....., k-1] = t[j-k, ......, j-1],那么我们就知道 t[0, 1, ....., k-1] 和 s[i-j, i-j+1, ..., i] 的前 k 个字符是匹配的(也就是t[0]=s[i-j], t[1]=s[i-j+1], ..., t[k-1]=s[i-j+k-1]),那么 j 只需要回退到 k+1 的位置(为什么 i 不需要回溯呢?会不会漏解?不会漏解,因为我们选择的是最长的公共前后缀)。
如下图所示,j 回退到 t[3] 的位置,i 可以保持不变。
生成next数组
最难理解的地方就是生成这个next数组。该数组的定义是 “当 s[i] != t[j] 时,j 回退到的位置为 next[j]”。从之前的分析可以知道,next[j]的值因该等于 t[0, 1, ......, j-1] 的最长公共前后缀的长度 k。如上面的例子,t[7]=d,t[0, ......, 6] 的最长公共前后缀长度为 3,所以 next[7]=3,也就是当 j 等于 7 发生 s[i]!=t[j] 时,j 应该回退到 t[3] 的位置。从next数组的定义可以知道,next数组只和模式串t有关系,跟 s 串没有关系,那也就说明不管 t 和谁去匹配,哪怕是和它自己去匹配(这一点很重要,对于为什么KMP算法中 k=next[k] 这一句的理解很重要),一旦发生失配,就让 j 回溯到 next[j] 的位置即可。
生成next数组的思路
public int[] getNext(String t) {
int[] next = new int[t.length()];
next[0] = -1;
int j = 1, k = 0;
while(j < t.length() - 1) {
if(k == -1 || t.charAt(j) == t.charAt(k)) {
j++;
k++;
next[j] = k;
} else {
k = next[k];
}
}
return next;
}
生成next数组的过程,就是 t 和自己进行比对的过程,这和 t 跟 s 的比对过程的思想是一样的。这其中的关键还是next数据的定义。也是此算法思想的巧妙之处。你可以理解为 j 是那一个不回退的 i,而 k 是那一个回退到 next[j] 的 j。因为next数组的生成只和 t 有关,不管 t 和谁比较,发生失配的时候,都是回退到 next[j] 处,这也就是为什么有那句 "k=next[k]"。这里我绕了好久才绕明白。/(ㄒoㄒ)/~~
public int indexOf(String s, String t) {
// KMP
int[] next = getNext(t);
// next[0] 和 next[1]都应该是零,这是显然的
next[0] = 0;
int j = 0;
for(int i = 0; i < s.length(); i++) {
if(s.charAt(i) == t.charAt(j)) j++;
else j = next[j];
if(j == t.length()) return i - j + 1;
}
return -1;
}