KMP算法是一个很经典的字符串匹配算法,以前只在算法课上写过作业,现在想起来,发现还是不会写了。所以再次温习记录一下。
字符串匹配
我们考虑如果不用KMP算法的话,暴力法做字符串匹配,简单写一下就是一个二重循环的写法,也就是,复杂度是相当高的。
int n_strStr(string str1, string str2) // 在str1 中匹配 str2 字符串
{
int len1 = str1.size(); // 原始字符串
int len2 = str2.size(); // 需匹配的字符串
int j = 0;
for(int i=0;i<len1;i++){
if(str1[i]==str2[0]){
for(j=1;i<len2;j++){
if(str1[i+j]!=str2[j]) break; // 中间没有匹配成功则重新匹配
}
}
if(j==len2) return i; // 匹配成功则直接返回原字符串中的位置,否则返回-1
}
return -1;
}
KMP算法
分析暴力的字符串匹配算法,发现每一次匹配未成功之后都是重新开始,从头开始匹配。这之间肯定有很多重复的比对工作,如果能通过一种方式将重复的比对避免过去,那这复杂度就会大大降低。KMP算法就是做这个事情的。KMP算法的核心就是匹配失败后,尽量减少再次匹配的次数,快速回溯,以实现字符串的快速匹配。
以栗子说明:主串为,待匹配项为,则如果是常规字符串匹配方法,则就算匹配到了,发生了不匹配,则是重新从开始匹配,主串往后跳一格,则是到。在KMP算法中,如果存在,因为已经和匹配,所以则可以直接得出,,即是待匹配项从开始匹配,而主串则还是在处等待匹配。
所以,KMP算法通过记录前后重叠子串,来实现发生不匹配时的快速回溯,KMP用一个next数组来记录这个数值。
这时候的字符串算法发生了改变:
int kmp_strStr(string str1, string str2) // 在str1 中匹配 str2 字符串
{
int len1 = str1.size(); // 原始字符串,主串
int len2 = str2.size(); // 需匹配的字符串
next[0] = -1; // 置第一位的next为-1
int j = 0;
for(int i=0;i<len1;){
/**
* j=-1表示已经回溯到了0,且发生了不匹配,这时候i也会向前增加;
* 发生匹配时,i,j 正常都增加
**/
if(j==-1 || str1[i]==str2[j]){
i++;
j++;
} else { // j!=-1,且发生了不匹配,则 j 进行回溯
j = next[j];
}
if(j==len2) return i-j; // 匹配成功则直接返回原字符串中的位置,否则返回-1
}
return -1;
}
next数组的求解
问题关键在于next数组的求解,因为next数组是代表当前子串的前后重叠字符串,所以其实本质上也是一个字符串匹配的过程,只不过这个把待匹配串在主串中匹配的位置固定了,简单来说就是:待匹配串是从0到i,在主串中匹配的位置是l-i到l。具体求解跟KMP算法一样,只不过这个需要求当前位置的next数组,具体见:
int main()
{
int len = pattern.size(); // pattern为待匹配项
next[0] = -1; // 置第一位的next为-1
int j = -1; //i 初始值要比 j 大1; next[1] = 0, 所以设置j从-1开始,这里区别KMP匹配过程
for(int i=0;i<len-1;){ // i 控制在 len-2,因为next[len-1]会在本次循环中求解出
/**
* j=-1表示已经回溯到了0,且发生了不匹配,这时候i也会向前增加;
* 发生匹配时,i,j 正常都增加
**/
if(j==-1 || pattern[i]==pattern[j]){
i++;
j++;
next[i] = j; // 因为 i-1 和 j-1 已经发生了匹配,则 next[j] 即是当前 i
} else { // j!=-1,且发生了不匹配,则 j 进行回溯;
j = next[j];
}
}
return 0;
}
可以发现和KMP算法匹配本身没有多大区别,就是一个 的初始值需要注意一下。利用next的定义,在求之前,和如果已经发生匹配,则说明,否则将 进行回溯。
文章参考:https://www.tomorrow.wiki/archives/1828
下一篇以LeetCode实例介绍KMP算法的应用。