BF
//利用字符数组,也可直接用字符串相关函数
static int indexOf(String text,String pattern) {
if(text==null||pattern==null) return -1;
char[] textchars=text.toCharArray();
char[] patternchars=pattern.toCharArray();
int tlen=textchars.length;
int plen=patternchars.length;
if(tlen==0 || plen==0) return -1;
if(tlen<plen) return -1;
int ti=0,pi=0;
while(ti<tlen && pi<plen) {
if(textchars[ti]==patternchars[pi]) {
pi++;
ti++;
}else {
ti-=pi-1;//找到主串重新开始的下一个位置
pi=0;
}
}
return pi==plen?ti-pi:-1;//返回子串在主串的位置
}
优化1:
//在主串中剩余长度小于子串长度的时候可以提前退出,减少比较次数
static int indexOf2(String text,String pattern) {
if(text==null||pattern==null) return -1;
char[] textchars=text.toCharArray();
char[] patternchars=pattern.toCharArray();
int tlen=textchars.length;
int plen=patternchars.length;
if(tlen==0 || plen==0) return -1;
if(tlen<plen) return -1;
int ti=0,pi=0;
int Distance=tlen-plen;
while(pi<plen && ti-pi<=Distance) {
if(textchars[ti]==patternchars[pi]) {
pi++;
ti++;
}else {
ti-=pi-1;
pi=0;
}
}
return pi==plen?ti-pi:-1;
}
如果上面的图不匹配,那下面就不用匹配了,直接退出循环
找到临界值10-4=6的位置,ti-pi是文本串正在匹配的子串的开始索引,当ti=7,pi=0时,没有比较的必要,立即退出
ti – pi 是指每一轮比较中 text 首个比较字符的位置,如8-2=6,符合要求,可以比较
优化2
static int indexOf3(String text,String pattern) {
if(text==null||pattern==null) return -1;
int tlen=text.length();
int plen=pattern.length();
if(tlen==0 || plen==0 || tlen<plen) return -1;
int tiMax=tlen-plen;
for(int ti=0;ti<=tiMax;ti++) {
int pi=0;
for(;pi<plen;pi++) {
if(text.charAt(ti+pi)!=pattern.charAt(pi))break;
}
//退出只有两个可能:1.pi==plen匹配完了 2.text与pattern不等
if(pi==plen) return ti;
}
return -1;
}
ti的含义:每一轮比较中文本串(主串)首个比较字符的位置
性能分析
最好情况
只需一轮比较就完全匹配成功,比较 m 次( m 是模式串的长度)
时间复杂度为 O(m)
最坏情况(字符集越大,出现概率越低)比如汉字比较
执行了 n – m + 1 轮比较( n 是文本串的长度)
每轮都比较至模式串的末字符后失败( m – 1 次成功,1 次失败)
时间复杂度为 O(m ∗ (n − m + 1)),由于一般 m 远小于 n,所以为 O(mn)
KMP
KMP的精妙之处:充分利用了此前比较过的内容,可以很聪明地跳过一些不必要的比较位置
next数组的使用
模式串的移动距离是以前比较的pi-next[pi]
核心原理
当 d、e 失配时,如果希望 pattern 能够一次性向右移动一大段距离,然后直接比较 d、c 字符
前提条件是 A 必须等于 B
所以 KMP 必须在失配字符 e 左边的子串中找出符合条件的 A、B,从而得知向右移动的距离
向右移动的距离:e左边子串的长度 – A的长度,等价于:e的索引 – c的索引
且 c的索引 == next[e的索引],所以向右移动的距离:e的索引 – next[e的索引]
总结
如果在 pi 位置失配,向右移动的距离是 pi – next[pi],所以 next[pi] 越小,移动距离越大,这样会忽略,所以要取最大公共子串长度
next[pi] 是 pi 左边子串的真前缀后缀的最大公共子串长度
前缀后缀的最大公共子串长度
为什么要将next数组的第一个赋值为-1呢?
next数组的目的是那个字符失配就去查那个字符在next表中的值。
0号位置失配,查表得-1,pi++后=0,模式串又从0开始比较
next表的代码实现
static int[] next(String pattern) {
int len=pattern.length();
int[] next=new int[len];
int i=0;
int n=next[i]=-1;
int imax=len-1;
while(i<imax) {
if(n<0||pattern.charAt(i)==pattern.charAt(n)) {
next[++i]=++n;
}else {
n=next[n];
}
}
return next;
}
反过来推,next[i]=n,说明i前面两个蓝色的A(包括A)的字符串最长公共子串是n。
如果i字符与n字符不相等,就要去找next[n]的最长公共子串,就是A(可以看到就是A的长度=k ),后面一个k是索引
如果i字符等于k字符,那next[i+1]=k+1
KMP主算法代码
static int KMP(String text,String pattern) {
if(text==null||pattern==null) return -1;
char[] textchars=text.toCharArray();
char[] patternchars=pattern.toCharArray();
int tlen=textchars.length;
int plen=patternchars.length;
if(tlen==0 || plen==0 ||tlen<plen) return -1;
int[] next=next(pattern);
int ti=0,pi=0,tmax=tlen-plen;
while( ti-pi<=tmax && pi<plen) {
if(pi<0 || textchars[ti]==patternchars[pi]) {
pi++;
ti++;
}else {
pi=next[pi];
}
}
return pi==plen?ti-pi:-1;
}