BF算法,俗称暴力匹配,枚举目标串和模式串的开头,依次向后匹配。时间复杂度高达O(mn)。
KMP算法:基于BF算法的优化,复杂度O(m+n)。所有算法的优化一定是基于去除冗余计算的。
先举个例子:目标串:ABCABCD 模式串:ABCD
BF算法第一步:ABCABCD
ABCD
适配,按照BF算法我们会将目标串的BCDA尝试与ABCD匹配,但是我们明显在第一步匹配时可以知道因为失配位置不是在B,所以目标串第二个位置必定是字符B,将A移动过去与之匹配是没有任何意义的。
KMP需要普及的一个概念是:相等的最长前缀与最长后缀的长度。
举个栗子:abcabcd,对于d来说,他前面的串为abcabc,相等的最长前后缀为abc。
注:1.我们这里所说的最长前缀不包括最后一个字符,最长后缀不包括第一个字符。
2.我们认为第一个字符由于前面没有字符,所以相等长度为-1,而第二个字符前面由于只有一个字符,所以认为最长前缀和最长后缀都是空串,最大公共长度为0。
我们将用此规则求出来的数称作是next数组。
以一个例子说明一下next数组:
a b a b a c
next: -1 0
此时,a前面只有为空串的前后缀或者是前缀为a,后缀为b,去相等时的最大值0。
a b a b a c
next: -1 0 0
此时,b前面最长相等的前后缀为a,长度为1。
a b a b a c
next: -1 0 0 1
此时,a前面最长相等的前后缀为ab,长度为2。
最终的next数组为:
a b a b a c
next: -1 0 0 1 2 3
如何利用next数组的信息加速?
现在我们已经有了next[v],那么我们可以知道在模式串中0~next[v]-1与v-next[v]~v-1是已经匹配了的。为了简便我们设v-next[v]=u。
如果我们在模式串的v位置与目标串的i+v位置第一次失配,那么我们可以知道前面的0~v的字符和目标串的i~i+v-1都是匹配的。而模式串本身的0~v-u位置的字符和u~v-1位置的字符是匹配的,而u~v-1位置的字符和i+u~i+v-1位置的字符匹配。根据等于的传递性可以得到模式串的0~v-u与目标串的i+u~i+v-1位置匹配。下次可以直接从模式串的v-u【即next[v]】与目标串的i+v位置进行匹配测试。
以实例做一次说明:
target :a b c a b c t
pattern:a b c a b c a
next :-1 0 0 0 1 2 3
如果是BF算法,下一步会变成:
target :abcabct
pattern: abcabca
而如果是KMP算法,加速过后的匹配为:
target :abcabct
pattern: abcabca
加速过程正确性的证明,即证明这种加速方法不会丢失匹配串,依旧以图为例:
加速过程否定了在i+1~u-1位置开头能与模式串完全匹配的可能性。
我们假设存在一个k,k∈(i+1,-1),使得以k位置开头的目标串能与模式串完全匹配。那么我们可以得出,从k出发到i+v-1这一段str1,设长度为len,与模式串的以0开始的长度为len的串str2【长度为len的前缀】匹配,而str1可以作为v的最长后缀,str2可以作为v的最长前缀,又因为k∈(i+1,-1),所以len>next[v],与next[v]的定义冲突,因此不可能存在一个k,k∈(i+1,-1),使得以k位置开头的目标串能与模式串完全匹配。
由此可以说明加速过程的正确性。
next[]表明了在失配时,应该用模式串的哪一个字符与目标串的哪一个字符继续匹配。
next数组求解过程:
规则:如果我们已经求出了next[i],当我们要求next[i+1]时:
1.如果str[i]==str[next[i]+1],我们可以得到next[i+1]=next[i]+1。
2.否则我们应该一直向前找,让next[i],继续进入判断
3.如果i=-1,那么此时我们将要求的next值设为
证明:
如图,当我们要求next[r+1]的时候,假设next[r]为i,即b前面的串有最长前缀0~i-1:
当我们要求next[r+1]的时候,有两种情况:
A.若i位置的字符为b,此时对a前面的串来说,可以在b的最长前后缀的最后接上一个字符b,这样next[r+1]=next[r]+1。
B.若i位置的字符不为b,此时由于next[r],我们可以知道str1和str2是相等的,也就可以知道str1的某些后缀和str2的某些后缀相等,此时根据next[i],我们可以求得str1中的最长公共前后缀str3,str4,由于str4==str5,str3==str4,根据传递性得到str3==str5,于是又回到了判断条件,这是一种递归过程。
C.若跳转回了-1,前面已经没有串,无法再匹配,则可以确定当前的next[r+1]=0。
next数组求解代码:
void getnext(string str){
Next[0]=-1;
Next[1]=0;
int i=2,cn=0;
while(i!=str.length()){
if(str[i-1]==str[cn]){
Next[i++]=++cn;
}
else if(cn>0){//cn还未跳转到0 因为0位置是没有前后缀的
cn=Next[cn];
}
else{//cn跳转到了0
Next[i++]=0;
}
}
}
匹配过程代码:
int match(string tar,string pattern){
if(pattern.length()>tar.length())return -1;
int i1=0,i2=0;
while(i1<tar.length()&&i2<pattern.length()){
if(tar[i1]==pattern[i2]){//匹配时继续向后试着匹配
i1++;
i2++;
}
else if(Next[i2]==-1){//失配时 如果模式串要跳转到-1号位置即等价于直接和目标串中当前位置下一个字符开始匹配
i1++;
}
else i2=Next[i2];//其他失配情况用next数组指明跳转
}
return i2==pattern.length()?i1-i2:-1;
}
改进KMP:
以一个字符串为例:a a a a c
求解next数组 : -1 0 1 2 3
如果我们匹配的是:
target : a a a a b a b c c
pattern:a a a a c
偷一张图:
在第二次匹配失败时,我们就已经知道,目标串中的失配字符不是a,那么三四五次匹配都是不必要的,然而朴素的KMP并不会去掉这些情况。
失配原因具体分析:
当第二次匹配失败时,我们已经可以知道t[4]=b,p[3]=a,p[3]!=t[4],而因为模式串i位置失配时我们是选择让模式串的next[i]与目标串中的适配字符继续匹配的,这个时候如果p[i]==p[next[i]],那么毫无疑问将发生失败匹配。
所以我们可以直接跳过这些情况:
pattern :a a a a c
next数组优化:-1 -1 -1 -1 3
优化next代码:
void getnext(string str){
next[0]=-1;
int i=1,cn=-1;
while(i!=str.length()){
if(cn==-1||str[i-1]==str[cn]){
if(str[i]!=str[cn+1])
next[i++]=++cn;
else{
next[i++]=next[cn++];
}
}
else{
cn=next[cn];
}
}
}
匹配和上述相同不再赘述。
参考:1.左神
2.https://segmentfault.com/a/1190000007066358 图解不错
3.https://www.cnblogs.com/cherryljr/p/6519748.html 对优化的讲解比较好
BM算法:
基于概率学的研究表明,从后向前的匹配比从前向后的匹配在失配时比较的字符要少得多,也能更快地跳转。统计的结果是一般BM算法会比KMP算法快3~5倍【大数据样本条件下】。
先看个BM算法的匹配例子:
target :HERE IS A SIMPLE EXAMPLE
pattern:EXAMPLE
第一次,模式串的E和目标串的S失配,此时观察到pattern中并没有S,所以我们可以一口气将这个串直接移动到S的后一个位置上,否则模式串都会和S发生交集,而因为模式串并没有S,所以可以直接判断这些情况中不可能发生完全匹配的情况。
target :HERE IS A SIMPLE EXAMPLE
pattern: EXAMPLE
第二次,模式串的E和目标串的E和目标串的L失配,同第一种情况,整体直接向右移动到L的下一个位置上。
由此我们先引出一条规则-坏字符规则:
当失配时,我们会得到将目标串中的失配字符,寻找该字符在模式串中的最右位置,让其与目标串的失配字符匹配,然后从模式串尾开始向前匹配。
好后缀规则:
1)如果有,则将整个模式串右移 最大索引-失配字符最右位置
target :HERE IS A SIMPLE EXAMPLE
pattern: EXAMPLE
第三次,目标串中失配字符为P,而模式串中P的最右位置为4,此时将P与失配字符对齐,可以得到我们是将模式串右移了6-4=2。即最大索引减最右位置,然后我们依旧从尾巴开始匹配,所以失配并移动后第一个匹配的位置是失配字符向右两个位置与模式串的最后一个字符匹配。
先说一下BM算法的简单实现。
我们当前有两个字符串,模式串p,目标串q,现在当我们发现了pi!=qj的时候:
A. 如果在模式串中,px==qj并且x<i,也就是x是i左边的离i最近的且等于qj的位置,那么为了使得qj得到匹配,应该将px拿过来,然后再重新从最后开始匹配。
B.如果没有这个px,那么可以简单地将模式串右移1【退化成BF算法】。
BM算法的优化实现比较难,我们来看个简单的。首先我们需要一个right[26]数组来保存模式串中每个字符出现的最右位置。
我们用两个指针i,j来指向两个串来实现匹配过程。
失配时,根据right数组的指向我们应该向前跳跃步数skip=j-right[target[j]-'a']
A.skip>0,这很显然表示和qj相等的px在i的左边,所以这个时候,我们只需要将模式串右滑skip。
B.skip<0,这很显然表示和qj相等的px在i的右边,所以这个时候,我们只能将模式串右滑1。