一、朴素算法
朴素算法就是最简单的暴力匹配算法,其步骤如下:
- 用两个指针 i,j 分别指向主串和模式串的第一个字符。
- 如果当前主串和模式串的字符相等,则两个指针后移,i ++,j ++。
- 如果当前主串和模式串的字符不相等,则 i 回到这一轮开始比较的字符的下一个字符 i - j + 1;j 则归零 j = 0,指向模式串的第一个字符。
- 如果最终 j 等于模式串的长度,则返回主串第一次出现模式串的位置 i - j,否则返回 -1。
假设主串的长度为 m,模式串的长度为 n,则该算法的时间复杂度是 O(mn)。
public static int violentSearch(String text,String pattern) {
int i=0,j=0;
while(i<text.length() && j<pattern.length()) {
if(text.charAt(i)==pattern.charAt(j)) {
i++;
j++; //两个字符串暂时相等,两个索引加一继续比较
}else {
i=i-j+1; //不能直接加一
j=0; //两个字符串出现不相等的字符,模式串索引归零,主串索引指向原地方下一个字符
}
}
if(j==pattern.length()) {
return i-j; //主串与模式串相等,返回B串第一次在A串出现的地方
}
return -1;
}
二、KMP算法
首先,用下面这个例子演示一下KMP算法是怎么工作的,如果看不懂可以先往下看。
在这里先解释一下什么是字符串的前缀和后缀。
存在字符串A和B,如果A=BS,其中S是任意的非空字符串,那就称B为A的前缀。
存在字符串A和B,如果A=SB,其中S是任意的非空字符串,那就称B为A的后缀。
以“abababca”为例:
- “a”的前缀和后缀都为空集,最大公共前后缀长度为0。
- “ab”的前缀为[a],后缀为[b],最大公共前后缀长度为0。
- “aba”的前缀为[a,ab],后缀为[ba,a],最大公共前后缀为“a”,长度为1。
- “abab”的前缀为[a,ab,aba],后缀为[bab,ab,b],最大公共前后缀为“ab”,长度为2。
- “ababa”的前缀为[a,ab,aba,abab],后缀为[baba,aba,ba,a],最大公共前后缀为“aba”,长度为3。
- “ababab”的前缀为[a,ab,aba,abab,ababa],后缀为[babab,abab,bab,ab,b],最大公共前后缀为“abab”,长度为4。
- “abababc”的前缀为[a,ab,aba,abab,ababa,ababab],后缀为[bababc,ababc,babc,abc,bc,c],最大公共前后缀长度为0。
- “abababca”的前缀为[a,ab,aba,abab,ababa,ababab,abababc],后缀为[bababca,ababca,babca,abca,bca,ca,a],最大公共前后缀为“a”,长度为1。
这时可以引入KMP算法的核心部分,一个被称为部分匹配表(Partial Match Table)的数组,在使用KMP算法进行匹配的时候我们需要用这个表来减少匹配的次数,我们可以将它命名为next数组。其实,“abababca”的next数组的值就是[0,0,1,2,3,4,0,1]。如果有 next [ j ]=k ,则表示0~j这个字符串的最大公共前后缀长度为k。例如,next [5]=4,“ababab”的最大公共前后缀长度为4。
如果用上面的方法将字符串的前后缀都写出来,肯定可以将一个字符串的部分匹配表也写出来,但是有点麻烦。那么现在我们用其他方法来求一下“abababca”的next数组。
- 建立一个next数组,长度等于模式串的长度,同时让next[0]=0。建立两个指针 i、j,分别指向模式串pattern的第二个字符和第一个字符。(实际上 i、j 指向同一个字符串,i指向子串的后缀,j指向子串的前缀。但是我们可以看做是两个字符串在匹配。)
- 此时 i=1,j=0,pattern[ i ] != pattern[ j ],失配。由于 j=0,不能再向左移了,所以将next[1]=0。保持 j 不动,i ++,相当于下面的字符串向后移一位。
- 此时 i=2,j=0,pattern[ i ] == pattern[ j ],匹配,将next[2]=1(这个1表示子串“aba”的最大公共前后缀为“a”,长度为1)。i ++,j ++。
- 此时 i=3,j=1,pattern[ i ] == pattern[ j ],匹配,将next[3]=2(这个2表示子串“abab”的最大公共前后缀为“ab”,长度为2)。i ++,j ++。
- 此时 i=4,j=2,pattern[ i ] == pattern[ j ],匹配,将next[4]=3(这个3表示子串“ababa”的最大公共前后缀为“aba”,长度为3)。i ++,j ++。
- 此时 i=5,j=3,pattern[ i ] == pattern[ j ],匹配,将next[5]=4(这个4表示子串“ababab”的最大公共前后缀为“abab”,长度为4)。i ++,j ++。
- 此时 i=6,j=4,pattern[ i ] != pattern[ j ],失配。注意这个时候 j!=0,j 左边0 ~ j-1的子串必定等于 i-j ~ i-1的子串(如果两个子串不相同,j 肯定就来不了现在这个位置了。也就是说,如果 j!=0,那么0 ~ j-1的子串必定等于 i-j ~ i-1的子串)。我们看到 j 前面的索引是3,next[3]=2,那么“abab”的最大公共前后缀为“ab”(ab是前缀也是后缀),长度为2。换句话说,由于本次比较已经将0 ~ j-1的子串和 i-j ~ i-1的子串进行了比较,而0 ~ j-1的子串等于 i-j ~ i-1的子串,同时,它们有最大公共前后缀“ab”。那么在下次比较中,就可以跳过最大公共前后缀“ab”(上面那个子串是后缀(下标4、5),下面那个子串是前缀(下标0、1)。即不用比较最大公共前后缀“ab”,因为它们已经比较过了,它们是相等的),让c(下标为6)与a(下标为2)比较。j=next[j-1](赋值前j=4,赋值后j=2),即 j 前移到2。(简单的说,j 跳到最大公共前缀的下一个字符)(个人的理解是,这就是为什么代码里j=next[j-1]要怎么写的原因)
- 此时 i=6,j=2,pattern[ i ] != pattern[ j ],失配。显然,0 ~ j-1的子串等于 i-j ~ i-1的子串。我们看到 j 前面的索引是1,next[1]=0,那么“ab”没有最大公共前后缀,长度为0。那么我们就必须让c(下标为6)与a(下标为0)比较。j=next[j-1](赋值前j=2,赋值后j=0),即 j 前移到0。
- 此时 i=6,j=0,pattern[ i ] != pattern[ j ],失配。由于 j=0,不能再向左移了,所以将next[6]=0。保持 j 不动,i ++,相当于下面的字符串向后移一位。
- 此时 i=7,j=0,pattern[ i ] == pattern[ j ],匹配,将next[7]=1(这个1表示子串“abababca”的最大公共前后缀为“a”,长度为1)。i ++,j ++。那么,到这里,next数组就求出来了。
看一下完整的过程:
现在我们再看一个例子:
我们只关注当 i=8,j=5,pattern[ i ] != pattern[ j ],失配的时候。
显然,这个时候下标3 ~ 7这个子串与下标0 ~ 4这个子串是相等的。我们看到 j 前面的索引是4,next[4]=2,那么“aabaa”的最大公共前后缀为“aa”(aa是前缀也是后缀),长度为2。所以下一次比较可以跳过“aa”(不用比较“aa”),j=next[j-1](赋值前j=5,赋值后j=2),将 j 前移到2,让a(下标为8)与b(下标为2)比较。
此时i=8,j=2,pattern[ i ] != pattern[ j ],失配。我们再看到b(下标为2)前面的a(下标为1)的值为1(next[1]=1),那么“aa”的最大公共前后缀为“a”,长度为1。所以下一次比较可以跳过“a”,j=next[j-1](赋值前j=2,赋值后j=1),将 j 前移到1,让a(下标为8)与a(下标为1)比较。这个时候,pattern[ i ] == pattern[ j ],所以next[8]=2。
现在我们就可以写出求next数组的代码了,现在估计可以理解代码了:
public static int[] getNext(String pattern) {
//获取next数组
int[] next = new int[pattern.length()];
next[0]=0; //初始化为0
for(int i=1,j=0;i<pattern.length();i++) {
while(j>0 && pattern.charAt(i)!=pattern.charAt(j)) {
j=next[j-1];
}
if(pattern.charAt(i)==pattern.charAt(j)) {
//前后缀字符相等
j++; //0~i这个字符串的最大公共前后缀的最大长度+1
}
next[i]=j; //该值表示0~i这个字符串的最大公共前后缀的最大长度
}
return next;
}
其实KMP算法的过程和求next数组的过程类似,只要理解next数组的求解过程其它的问题就没这么难了。我们再看一下下面如何利用KMP算法匹配字符串:
public static int kmp(String text, String pattern) {
int[] next = getNext(pattern);// 计算next数组
for(int i=0,j=0;i<text.length();i++) {
while(j>0 && text.charAt(i)!=pattern.charAt(j)) {
j=next[j-1];
}
if(text.charAt(i)==pattern.charAt(j)) {
j++;
}
if(j==pattern.length()) {
//匹配成功
return i-j+1;
}
}
return -1;
}
假设主串的长度为 m,模式串的长度为 n,则该算法的时间复杂度是 O(m+n),空间复杂度为 O(n)。
三、参考文章和视频
1. 汪都能听懂的KMP字符串匹配算法【双语字幕】(本文的例子参考了这个视频,真的讲的很好,推荐!!!)
2. 如何更好的理解和掌握 KMP 算法?海纳的答案(知乎)
3. KMP算法详解-彻底清楚了(转载+部分原创)(详细)
4. KMP算法最浅显理解——一看就明白
如有错误请大家指出了(ง •̀_•́)ง (*•̀ㅂ•́)و