KMP算法
解决的问题:给定字符串s(长度为n)和模式串f(长度为m),判断f是否在s中出现。如果存在,则返回f首次出现的位置。
子字符串通用的方法是遍历目的串和模式串,先遍历目的串s,然后遍历模式串f,复杂度是O(n*m)。
KMP算法,利用构造跳转表next,字符串的前后缀。
目的串s为:BBCABCDAB ABCDABCDABDE
模式串f为:ABCDABD
匹配过程:
1、从字符从s的第一个字符开始与模式串比较,s[0]= 'B',f[0]= 'A',不匹配,向后搜索直到第一个相匹配的位置。
BBCABCDABABCDABCDABDE
ABCDABD
2、s[3]= f[0] = 'A',接着搜索s和f的下一个字符,
s[4]= f[1] = 'B' s[5] = f[2] = 'C' s[6] = f[3] = 'D' s[7] = f[4] = 'A'
s[8]= f[5] = 'B' s[9] = 'A' != f[6] = 'B'
BBCABCDABABCDABCDABDE
ABCDABD
3、如果按照遍历的方法,s应该搜索下一个字符串,并与f的第一个字符相比较
s[4]= 'B' f[0] = 'A' 不相等,继续向后搜索,
BBCABCDABABCDABCDABDE
ABCDABD
直到下一个相同的字符,s[7]= 'A' f[0] = 'A'
BBCABCDABABCDABCDABDE
ABCDABD
实际上,在步骤2中已经比较了模式串f字符串中的这一部分,步骤3中又重复比较了这一部分的字符,重复了这一部分。
4、KMP算法利用了已经比较的模式串f的字符信息,跳过步骤3,直接比较s[7]= f[0]。
如何直接跳到s[7],就要利用下面的部分匹配表了。
字符串的前缀、后缀。
BBCABCDABABCDABCDABDE
ABCDABD
5、字符串的后缀与 最长前缀 相匹配的长度
部分匹配表
模式串f A B C D A B D
部分匹配值 0 0 0 0 1 2 0
后缀和前缀均不包括满字符串,判断后缀与前缀相匹配的最大长度,最长前缀
i=0 A 后缀:无前缀:无 前缀后缀最长匹配:0
i=1 AB 后缀:B 前缀:A 最长匹配:0
i=3 ABC 后缀:CBC 前缀:AAB 最长匹配:0
i=4ABCD 后缀:DCD BCD 前缀:AAB ABC 最长匹配:0
i=5ABCDA 后缀:ADA CDA BCDA 前缀:AAB ABC ABCD 最长匹配:1(A)
i=6ABCDAB 后缀:BAB DAB CDAB BCDAB
前缀:AAB ABC ABCD ABCDA 最长匹配:2(AB)
I=7ABCDABD 后缀:DBD ABD DABD CDABD BCDABD
前缀:AAB ABC ABCD ABCDA ABCDAB 最长匹配:0
得到如下的部分匹配表
部分匹配表
模式串f A B C D A B D
部分匹配值 0 0 0 0 1 2 0
6、继续解释步骤4
当步骤2中得到 s[3]= f[0] = 'A' s[9] = 'A' f[6] = 'D' 不相等时,
BBCABCDABABCDABCDABDE
ABCDABD
根据步骤5中的部分匹配表,f字符串中f[6]='D'时不匹配,不包含f[6]字符串,f[0]~f[5]中最长前缀后缀匹配为2
i=6ABCDAB 后缀:BAB DAB CDAB BCDAB
前缀:AAB ABC ABCD ABCDA 最长匹配:2(AB)
字符串s移动的位数为:
移动位数=已匹配的字符数– 对应的部分匹配值
4 = 6 - 2
将s向后移动4位,如下。s[7]= f[0] = 'A' s[8] = f[1] = 'B'
s[9]= 'A' f[2] = 'C'不相等
BBCABCDABABCDABCDABDE
-- - -ABCDABD
7、继续利用KMP算法
s[9]= 'A' f[2] = 'C'不相等 不包含f[2]='C',查找部分匹配值
i=1 AB 后缀:B 前缀:A 最长匹配:0
字符串s移动的位数为:
移动位数=已匹配的字符数– 对应的部分匹配值
2 = 2 - 0
字符串s移动的位数为2
BBCABCDABABCDABCDABDE
-- ABCDABD
8、继续利用KMP算法s[11]= 'A' f[0] = 'A'
s[17]= 'C' f[6] = 'D'不相等 不包含f[2]='D',查找部分匹配值
i=1 AB 后缀:B 前缀:A 最长匹配:0
字符串s移动的位数为:
i=6ABCDAB 后缀:BAB DAB CDAB BCDAB
前缀:AAB ABC ABCD ABCDA 最长匹配:2(AB)
字符串s移动的位数为:
移动位数=已匹配的字符数– 对应的部分匹配值
4 = 6 - 2
将s向后移动4位,
BBCABCDABABCDABCDABDE
-- - - ABCDABD
完全匹配,搜索结束。
9、后缀和前缀
前缀"指除了最后一个字符以外,一个字符串的全部头部组合;
"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
10、KMP算法中的next数组,相当于最大前缀后缀右移一位,初始值设为-1。向右移动位数为:j - next[j]。其中,j为模式串中不匹配字符的位置,且j从0开始计数。
模式串f A B C D A B D
最大前缀后缀 0 0 0 0 1 2 0
next数组 -10 0 0 0 1 2
为什么有了最大前缀后缀数组,还要采用next数组呢?因为数组默认是从0开始,而最大前缀后缀数组中不匹配的字符为f[6]= 'D'时,需要得到前一位'B'对应的最大前缀后缀长度2,而利用next数组右移一位后,则可以直接采用next[6]= 2
感觉用最大后缀前缀数组比较容易理解。
// 很巧妙的获取next数组,不是很好理解
void getNext(char* p, int next[])
{
int pLen = strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1)
{
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p[j] == p[k])
{
++k;
++j;
next[j] = k;
}
else
{
k = next[k];
}
}
}
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)
{
if(j == -1 || s[i] == p[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
if (j == pLen)
return i - j;
else
return -1;
}