经典算法的第一篇,记录一下KMP算法好了,因为比较经典巧妙而且我考研的时候忘看了没做出来???。KMP算法是三个人一起发明的,三个人的名字首字母分别是K、M、P,所以叫KMP~
问题定义
给定一个字符串S, 以及另一个字符串P, 判断P是否为S的子串,如果是的话输出第一个匹配到的位置。
暴力解法
暴力解法比较容易想到,就是遍历S,如果找到和P的首字母相匹配的元素,就继续逐个比对,发生失配时就继续遍历S,直到找到完全匹配的子串。如果遍历完了还找不到,就返回false。过程如下(本篇笔记里的所有动图都是从另一篇博客搬过来的,博客地址是这个):
优化空间
暴力解法中可以进一步进行改进的地方在于,当在S中遇到一个与 P[0] 匹配的字符,逐个比对的时候,已经匹配了若干个字符,获得了一些P的信息,一旦发生失配就彻底丢失这些信息,遍历到S的下一个字符开始从头匹配。
暴力解法忽略了一种情况,就是P中已知的信息是可以对下一步的遍历进行指导的,具体来说也就是:
当P中前k个已经匹配成功时,我们可以观察这k个元素内部,有没有首尾相同的元素,比如:如果这K个元素是 {a, b, c, d} 这样的元素(首尾没有重复的元素),那么便没有必要继续在S中 {b, c, d} 这k-1个元素的地方进行匹配了。从反面情况来解释,当我们有必要在这k个元素里以其中某一个(设为ki)为首挨个匹配时,说明从ki到kk(k个元素里的最后一个)跟P的前 k-i+1 个元素都是匹配的,又因为P的前 k-i+1个元素等于K中的k1,…,kk-i+1,所以也就是:ki,…, kk = k1,…,kk-i+1,即首尾重叠。而首尾无重叠的时候,可以预见,没有必要在这k-1个元素里再比对。
动图更加直观,看下面???
自然可以想到,每次发生失配的时候,计算出此时已匹配的K个元素里的最大首尾重叠元素数d,将模式串P右移K-d个位置再次从上次失配的地方开始匹配就行。
在KMP算法里,用了一个next数组来实现。next数组的长度是模式串P的长度,next[j]存储的是第j个元素之前的字符串里的最大首尾重叠元素数。当S[i]与P[j]处发生失配时,这个时候,S[i]前面的d个元素和P[next[j]]前面的d个元素是一样的,令j = next[j], i不变,再继续挨个匹配就行。注意next[0] = -1,因为如果设next[0]为0的话,如果第一个元素就失配,i不变,j=0,相当于永远就停在这个地方不往前走了。使next[0] = -1并且当j ==-1时 i++,j++,这样就会继续遍历。
KMP的代码如下,还是比较简洁的:
int KmpSearch(char* s, char* p)
{
int i = 0;
int j = 0;
int sLen = strlen(s);
int pLen = strlen(p);
while (i < sLen && j < pLen)
{
//①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
if (j == -1 || s[i] == p[j])
{
i++;
j++;
}
else
{
//②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
//next[j]即为j所对应的next值
j = next[j];
}
}
if (j == pLen)
return i - j;
else
return -1;
}