给两个字符串s和t,如何很快的知道s是否包含t(即t是否是s的子串)。暴力的方法,我们依次以s每个位置为头,去匹配t。
public int find(String s, String t) {
char[] ss = s.toCharArray();
char[] tt = t.toCharArray();
int i = 0;
while (i < ss.length) {
int j = 0;
//以s的每个位置为头匹配
int start = i;
while (start < ss.length&& j < tt.length) {
//有一个不等直接break,匹配下一个位置开头的串
if (ss[start] != tt[j]) {
break;
}
start++;
j++;
}
//如果j来到了t字符串的长度,说明上面的循环第一次匹配上了,返回第一次匹配上的位置下标
if (j == tt.length) {
return start-tt.length;
}
//否则,从下一个位置开始匹配
i++;
}
return -1;
}
举例,s=ababeababde,t=ababd。我们看一下暴力解法的过程。
1、i=0,start=0,j=0去循环匹配。start=4,j=4时,不匹配,break。start回退到1,j回退到0位置。
2、i=1,start=1,j=0去循环匹配。不匹配,break。start到2,j回退到0位置。
3、i=2,start=2,j=0去循环匹配。不匹配,start=4,j=2时不匹配,break。start回退到3,j回退到0位置。
4、i=3,start=3,j=0去循环匹配。不匹配,break。start到4,j回退到0位置。
5、i=4,start=4,j=0去循环匹配。不匹配,break。start到5,j回退到0位置。
5、i=5,start=5,j=0去循环匹配。匹配完成。(省略中间过程)。
从上述过程中可以看到,已经遍历过的位置会多次回退,无疑增加了时间复杂度。KMP算法过程和暴力遍历时相似的,只是在过程中对回退操作进行了优化,减少了回退。
了解KMP算法前,我们先了解一个定义:最长前后缀相等的长度(前缀和后缀相等的最大长度),对于字符串t,我们生成一个next数组,数组每个位置表示该位置之前的字符串中前缀和后缀相等的最大长度(规定:不能取到字符串最大长度,如果该位置前面没有字符串,最长前后缀相等的长度为-1(人为规定))。
比如上述字符串t=ababd,其next数组为 int[] next=new int[]{-1,0,0,1,2}。假设我们已经有了next数组,我们再看一下上面例子的匹配过程怎么加速?(后续会将next数组如何生成)
1、以0位置开头去匹配,当s来到start=4位置的e时,t位置来到j=4位置的d时,发现不匹配,我们检查next数组,发现4位置的最长前后缀相等的长度(next[4])是2,那么t位置回退到j=next[4]=2,继续开始比较;【为什么可以回退到这进行比较呢?因为s的前4个字符 abab,t的前4个字符 abab 已经比较过了,是相等的,而j=4时next[4]告诉我们当前位置前面字符串的最长前后缀相等长度是2,也就是说t[1]=s[start-1],t[0]=s[start-2],所以这两个位置就不需要再比较了,start就不需要进行回退了,j位置也可以只回退到next[4]位置。】其实此时的含义是,看以start-2位置开头的字符串是否可以匹配出t。
2、start=4,j=2位置开始比较,发现不匹配,此时j=2位置的最长前后缀相等的长度(next[2])是0,所以t的位置回退到j=next[2]=0;
3、start=4,j=0位置开始比较,发现不匹配,此时j已经为0,j无法再回退,所以satrt++;
4、start=5,j=0位置开始比较,匹配完成(后续过程省略)。
从上述过程我们可以看出,s的遍历位置没有回退。下面我们看一下KMP的代码。
public int KMP(String s,String t){
if(s==null||n==null||s.length()<t.length()){
return -1;
}
char[] ss=s.toCharArray();
char[] tt=t.toCharArray();
int N=tt.length;
//获取t字符串的next数组 next[i]表示t字符串【i位置以前的最长前后缀相等长度】
int[] next=getNextArray(t);
int i=0;
int j=0;
while(i<ss.length){
if(ss[i]==tt[j]){
i++;
j++;
}else if(j>0){
//t的位置可以回退,回退到当前位置的最长前后缀长度的下一个位置进行比较
j=next[j];
}else{
//j=0&&ss[i]==tt[j]
i++;
}
}
return j==tt.length?i-j:-1;
}
关键问题来了,我们如何得到next数组呢?根据规定next[0]=-1,next[1]=0,next[2]=tt[0]==tt[1]?1:0。那么我们考虑一个普遍位置i(即考虑t字符串0-i-1位置字符串的前缀后缀相等情况),假设next[i-1]位置已经计算好了。如下图所示。那么就有以下几种情况:
1、tt[i-1]=tt[next[i-1]](tt[next[i-1]]即左侧的三角位置,next[i-1]标识的两段根据其定义知道相等),所以此时可得出 next[i]=next[i-1]+1;
2、tt[i-1]!=tt[next[i-1]],那如何寻找下一个比较位置呢?我们知道▲位置的next[▲]是该位置的最长前后缀相等长度,那么下一个比较位置就是★标识的位置,而★=next[▲]。以此类推,直到来到0位置,如果还不相等,那就代表i位置以前字符串的最长前后缀相等长度为0。
如此下去,我们得到字符串t的next数组。(最长前后缀相等长度数组)。下面请看代码。
public int[] getNextArray(String t){
if (t.length() == 1) {
return new int[]{-1};
}
if (t.length() == 2) {
return new int[]{-1, 0};
}
char[] tt = t.toCharArray();
int[] next = new int[t.length()];
//规定值
next[0] = -1;
next[1] = 0;
if (tt[0] == tt[1]) {
next[2] = 1;
}
//从2位置开始计算
int i = 2;
int compareIndex=next[i-1];
while(i<tt.length){
if(tt[i-1]==tt[compareIndex]){
//相等,next[i]就计算出来了,i++计算下一个位置,compareIndex++是next[i]的值
//也是计算i+1位置时,第一个需要比较的位置,大家多画图就可以理解了
next[i++]=compareIndex++;
}else if(compareIndex>0){
//计算下一个比较位置
compareIndex=next[compareIndex];
}else{
//compareIndex==0&&tt[i-1]!=tt[compareIndex]
next[i]=0;
//求下一个位置
i++;
}
}
return next;
}
到此整个KMP算法就结束了,了解了原理后,代码还是很好写的。