引入
学习一个算法要先知道它是什么,KMP算法是一种改进的字符串匹配算法。那么之前我用什么做字符串匹配呢?暴力破解,一直DF。
假设主串是ababababca,模式串是abababca。
DF算法(暴力匹配)
让模式串和主串从第一个开始匹配,如果相同,就接着比较后面的,如果不同模式串下标回到0,主串下标回到第一个匹配的位置的下一个接着比较。
算法复杂度最后为 O(m*n)
KMP算法
KMP算法的核心,是一个被称为部分匹配表(Partial Match Table)的数组。对于模式串abababca的PMT数组如图。
其中index就是在模式串的下标,char就是下标对应的字符,value就是 字符串的前缀集合与后缀集合的交集中最长元素的长度。
那字符串的前缀和后缀是什么?
如果字符串A和B,存在A=BS,其中S是任意的非空字符串,那就称B为A的前缀。例如,"Harry"的前缀包括{“H”, “Ha”, “Har”, “Harr”},我们把所有前缀组成的集合,称为字符串的前缀集合。同样可以定义后缀A=SB, 其中S是任意的非空字符串,那就称B为A的后缀,例如,"Potter"的后缀包括{“otter”, “tter”, “ter”, “er”, “r”},然后把所有后缀组成的集合,称为字符串的后缀集合。
那PMT的值有什么作用呢?
前缀和后缀是abab和abab,所以PMT是4。
而主串和模式串前面都是匹配过的,是相同的。在模式串中,C的前面四个字符和下标为4的字符前面四个元素是一样的。因为下标为4的元素的前四个就是和它相等的C前一个元素的后缀。所以最终模式串下标为4的元素的前面四个元素和主串此时下标前面的四个元素是相同的。
所以模式串不需要在退回到下标为0,只需要下标移到对应的PMT的值就可以了,主串下标不需要移动。
刚才我们真正比较时,用到的是前面一个字符对应的PMT的值,所以我们需要把PMT的值向后移一位得到真正使用的next数组,首位设为-1是为了区分首位。
KMP核心代码
int KMP(char * t, char * p)
{
int i = 0;
int j = 0;
while (i < strlen(t) && j < strlen(p))
{
if (j == -1 || t[i] == p[j])
{
i++;
j++;
}
else
j = next[j];
}
if (j == strlen(p))
return i - j;
else
return -1;
}
那在代码中如何快速求出next数组的值呢?
我们可以采取模式串自己和自己匹配的方法。具体来说,就是从模式字符串的第一位(注意,不包括第0位)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next值。如下图所示。
求next数组代码
void getNext(char * p, int * next)
{
next[0] = -1;
int i = 0, j = -1;
while (i < strlen(p))
{
if (j == -1 || p[i] == p[j])
{
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
}
KMP最后的算法复杂度为 O(m+n)
总结
KMP相比暴力DF算法复杂度更加低,但是理解比较难。