字符串匹配算法(BF算法和KMP算法)
- BF算法
当我们进行字符串的匹配时,最直接的方法就是主串字符与模式串字符逐个比较,如图所示:
教材指出,BF算法低效且会导致主串指针回溯,可上述例子中主串指针并未发生回溯,我在学习中也产生了疑惑,究其原因,还是例子具有偶然性
看如下例子:
指针发生了回溯。但对于上述例子我又产生了疑问,为什么模式串不能直接后移到如下图所示位置?
这样不仅主串不用回溯,效率还高,这个问题需要结合代码来看
public static int indexOf_bf(String target, String pattern, int begin){
int n=target.length();
int m=pattern.length();
if (begin<0){
begin=0;
}
if (n==0 || n<m || begin>=n){
return -1;
}
int i=begin;
int j=0;
while(i<n && j<m){
if (target.charAt(i)==pattern.charAt(j)){
i++;
j++;
}
else {
i=i-j+1; //回溯
j=0;
if(i>n-m){
break;
}
}
}
return j==m ? i-m : -1;
}
上述代码中,i=i-j+1实际上就是执行主串回溯,可如果要实现上图假设,让主串指针i不发生回溯,则应去掉i=i-j+1这行代码,才能满足假设。else内语句变为:
else {
j=0;
if(i>n-m){
break;
}
}
此时指针位不动,j=0,将会陷入死循环。
上述的假设已经有KMP算法的影子,我们怎么能让主串指针不回溯从而进行字符串匹配呢?
- KMP算法
通过上述的假设,我们发现,在匹配时主串”abccabcd“前三个元素和模式串”abcd“前三个元素相等,当第四个元素不相等时,我们可以直接将模式串后移到如下图所示位置,而不需要仅仅向BF算法一样仅移动一位
而KMP算法就是这个作用,KMP算法中的next数组,则是用来注明当主串与模式串特定位置的字符发生不匹配后,模式串应该移动到什么位置
在KMP算法中,引入了最长前缀和最长后缀的概念,如下图,当主串4号位与模式串4号位不匹配时,我们需要后移模式串,因为在模式串的子串”abca“中,最长前缀=最长后缀的情况是:模式串[0]=模式串[3],所以模式串可直接后移到如下图所示位置
此时next[4]=1,思考时,我们可以把这个过程想象为模式串后移,可在计算机中,next[4]=1的含义是当模式串4号位与主串不匹配时,主串当前位与模式串1号位比较
同理,当主串5号位与模式串5号位不匹配时,最长前缀=最长后缀的情况是:模式串[0],模式串[1] = 模式串[3],模式串[4],next[5]=2,后移结果如图所示
以下是KMP算法以及求next数组的代码实现
public static int indexOf_kmp(String target, String pattern, int begin){
int n=target.length();
int m=pattern.length();
int[] next;
if (begin<0){
begin=0;
}
if (n==0 || n<m || begin>=n){
return -1;
}
next=getNext(pattern);
int i=begin;
int j=0;
while(i<n && j<m){
if (j==-1 || target.charAt(i)==pattern.charAt(j)){
i++;
j++;
}
else{
j=next[j];
if (n-i+1<m-j+1){
break;
}
}
}
return j==m ? i-m : -1;
}
求next数组
public static int[] getNext(String pattern){
int j=0;
int k=-1;
int[] next=new int[pattern.length()];
next[0]=-1;
while(j<pattern.length()-1){
if (k==-1 || pattern.charAt(j)==pattern.charAt(k)){
j++;
k++;
next[j]=k;
}
else{
k=next[k];
}
}
return next;
}
想要理解求next数组的实现代码,我们首先要清楚在求next数组时,使用了递归的思想。
算法过程:
1> 约定next[0]=-1,-1表示下次匹配从主串当前位的下一位开始比较,则有next[1]=0
2> 对模式串中j号位字符,设next[j]=k,表示在 模式串[0]~模式串[j-1] 中存在长度为k的相同前后字符串,即 模式串[0]~模式串[k-1] = 模式串[j-k]~模式串[j-1] ,k取得是最大值
2>对于next[j+1]而言,求 模式串[0]~模式串[j] 中相同前后缀子串的长度k,需要比较前缀子串 模式串[0]~模式串[k] 与后缀子串 模式串[j-k]~模式串[j] 是否匹配,这又是一个模式匹配问题
1.如果 模式串[k]=模式串[j] 即 模式串[0]~模式串[k-1],模式串[k] = 模式串[j-k]~模式串[j-1],模式串[j] 存在相同前后缀子串,长度为k+1,则 模式串[j+1] 的next[j+1]=k+1=next[j]+1
2.如果 模式串[k] != 模式串[j] ,在 模式串[0]~模式串[j] 中寻找较短的相同前后缀子串,较短前后缀子串长度为next[k],则k=next[k]
但是,在实际计算过程中,next数组的计算算法还是有些问题,如下图所示,会出现某些不必要的比较
改进next数组算法如下:
public static int[] getNext_improve(String pattern){
int j=0;
int k=-1;
int next[]=new int[pattern.length()];
next[0]=-1;
while(j<pattern.length()-1){
if (k==-1 || pattern.charAt(j)==pattern.charAt(k)){
j++;
k++;
if (pattern.charAt(j)==pattern.charAt(k)){ //增加部分
next[j]=next[k];
}
else{
next[j]=k;
}
}
else{
k=next[k];
}
}
return next;
}