前言:
记住代码并非学懂了算法,而是学会了做题。
真正学懂算法是能够教会别人。
引例:给定两个字符串S和T,问T是否属于S的子串。
这就是基本的字符串匹配问题,我们很容易想到暴力破解法。
暴力破解法:
为便于描述和理解,我们称S串为主串,T串为模式串。
在主串中枚举模式串的起点,若字符相同则继续向后匹配,不同则更新起点,重新开始匹配。
定义指针i、j
i:正在匹配主串第i个字符
j:已经匹配了模式串前j个字符
初始状态
主串待匹配字符s[i]='A'与模式串下一个字符t[j+1]='A'相同,匹配成功。指针后移,i++;j++。
同理,再经过一次匹配后,
主串待匹配字符s[i]='B'与模式串下一个字符t[j+1]='A'不同,匹配失败。更新枚举起点i=2,重新开始匹配,直到模式串已匹配字符数j==len(模式串长度)为止。
估算时间复杂度:O(n*m)
KMP算法:
KMP算法的本质是对暴力破解法的优化。删减无效操作,将可用信息重复利用。
我们先继续对上述过程的模拟,
经过数次更新起点与匹配后,我们得到以上状态。按照暴力破解法的流程,此刻匹配失败,由i=4为起点枚举而来,因而更新起点至i=5。
但实际上并不需要这样的操作,我们观察:
设k,s[i-k]..s[i-2]s[i-1]=t[1]..t[k-1]t[k](s<i-k<=i-1)
若k不存在,那么以任何一点d(s<d<=i-1)为起点都无法匹配出模式串。①
若k存在:
若t[k+1]!=s[i],那么也无法匹配出模式串。②
若t[k+1]=s[i],则有可能以i-k为起点匹配出模式串。
①:都会因s[i-1]或之前的某个字符失配而断掉。
②:会因s[i]失配而断掉。
图1:不存在满足条件的k,从任意起点p(s<p<=i-1)都因某字符q匹配不到i-1
图2:存在满足条件的k,但t[k+1]!=s[i],因字符i匹配不到i-1
图3:既存在满足条件的k,t[k+1]=s[i],那么则有可能以i-k为起点匹配出模式串
因而,当匹配失败时,只需找到满足条件的k的最大值。若存在,主串向后匹配,以i-k为起点;若不存在,主串也向后匹配,寻找新的起点。
请思考,选择最大的k是否会漏掉可能的匹配起点?(答案见下文)
算法实现:
我们知道:s[s]..s[i-2]s[i-1]=t[1]..t[j-1]t[j]
因为s<i-k<=i-1,
所以s[i-k]..s[i-2]s[i-1]=t[j-k+1]..t[j-1]t[j]
因而我们只需要在模式串已匹配部分寻找k即可。
由于通常情况下模式串长度比较小,我们可以初始化出模式串中每个位置的k值。我们常用next[]数组存储模式串中每个位置的k值。
那么如何求得next[i]?
其实我们求得next[i]的过程等价于用模式串匹配自身的过程。为位置i寻找最长前缀后缀,可以想成用i前面的字符串去匹配模式串,到i最多能够匹配多长。这样以来问题就迎刃而解了,我们只需要进行两次匹配,一次求得next数组,一次匹配模式串。
注意:满足条件的k是指由i-k到i-1能够匹配模式串前k个字符,同时s[i+1]=t[k+1]!
算法流程:
1.初始化指针i=1,j=0
2.循环枚举主串待匹配字符i
3.枚举一个字符就对模式串进行一次匹配:
若s[i]=t[j+1],则模式串第j+1个字符匹配成功,i++;j++
若s[i]!=t[j+1],匹配失败,则更新j等于最长的满足条件的k,i++
4.当j=len时,匹配成功,跳出循环。
代码如下:
1 void get_next() 2 { 3 int i,j; 4 j=0; 5 next[1]=0; 6 for(i=2;i<=lent;i++){ 7 while(j>0&&t[j+1]!=t[i])j=next[j]; 8 if(t[j+1]==t[i])j++; 9 next[i]=j; 10 } 11 } 12 13 void KMP() 14 { 15 int i,j; 16 j=0; 17 for(i=1;i<=lens;i++){ 18 while(j>0&&t[j+1]!=s[i])j=next[j]; 19 if(t[j+1]==s[i])j++; 20 if(j==lent)ans[++cnt]=i-j+1; 21 } 22 }
next数组的求得很关键,这几天要期末考试,学业繁重。日后加图完善!