目录
朴素算法(BF)进行模式串匹配的思想为将模式串与主串进行字符比较,如果相等,那么模式串和主串中对应位置的指针均往后前进一步,如果模式串中所有的字符都匹配成功了,则表示主串中包含模式串,但如果在某一位置不等,则从主串的下一位置开始重新和模式串进行比较,如果指向主串的指针越界,咋表示匹配失败。BF算法其思想比较简单,实现过程也并不复杂,网上相关博客很多,这里不做赘述,如果读者不懂BF算法,可进行查阅分析。好了,言归正传,首先看一个例子:
主串:s1 = "000000000000000000001";
模式串:s2 = “000001”;
按照BF算法的思想,那么在主串和模式串匹配过程中,每一次都在模式串最后一个元素处失效,总的时间复杂度On(n*m)。(其中n是主串的长度,m是模式串的长度)。但是实际上我们看在最后一个位置m处失效,那么前m-1个位置肯定是相等的,按照上述给出的例子,在匹配失效后,主串i回退到第二个位置,模式串从起始位置开始,重新进行匹配,如图所示我们发现,其实存在一些第一次已经比较过相等的数字了,再进行比较无疑是多余的。KMP算法就是为了解决这种问题而产生的,我个人认为是对BF算法进行的优化操作。
再具体说明KMP算法之前,首先需要了解一下什么叫做字符串的前缀和后缀。关于前缀和后缀的标准定义读者可以在网上进行查找,我这里仅仅说明一些如何找前缀和后缀。
例如字符串S3 = “hello”;那么前缀就是从第一个字母开始,但不过包含最后一个字符的序列,如{“h”,“he”,“hel”,“hell”};
那么后缀呢,其实和前缀相反,表示从字符串的最后一个字串穿开始,但是不包括第一个字符的序列,如{“ello”,“llo”,“lo”,“o”};
说明了字符串的前缀和后缀后,下面我们来看看next数组的求解过程。KMP算法的优化之处在于主串的指针不进行不必要的回退,模式串的指针也并不是完全退回到起始位置,而是退回到k位置。看到这里,读者肯定心里犯嘀咕了,k位置我咋知道是那个位置,别急,下面关于k我们来好好认识一下他的真实面目。
如图所示,当主串和模式串在红色区域失效后,j应该返回到哪里呢?
很明显j应该返回到第一个位置上,为什么不返回到第一个位置上呢?因为前面正好有一个A相同不是吗。
再看一个例子:
如图所示j应该返回到第二个位置上,因为前两个位置A和B是相同的,如图所示:
你有发现什么问题吗?没错,j返回的位置其实就是在模式串失效位置的前子字符串的最大公共前缀和后缀的个数。这句话很重要,读者应仔细理解。说到这里,你应该明白了k是什么了吧,k其实表示的就是j在匹配失效时j返回到字符串的k位置上。如图所示:
下面咱们来手动推算一个字符串s4 = "abcab"的next数组。
next[0] = 0; //只有一个字符,next[0] = 0;
next[1] = 0; //ab无公共前缀后缀,next[1] = 0;
next[2] = 0; //abc无公共前后缀,next[2] = 0;
next[3] = 1; //abca有公共前后缀{a},next[3] = 1;
next[4] = 2; //abcab有公共前后缀{ab},next[4] = 2;
下面给出求next数组的代码:
void getnext(string str, vector<int>& next) {
int len = str.size();
next[0] = -1;
int i = 0; //i遍历模式串
int j = -1; //j统计公共前缀和后缀的个数
while (i < len - 1) {
if (j == -1 || str[i] == str[j]) {
next[++i] = ++j;
}
}
else {
j = next[j];
}
}
}
读者看到这里估计又蒙蔽了,老哥啊,我刚刚看的有点感觉了,你整个这,next[++i] == ++j,j = next[j]这都是啥玩意啊,看不懂啊,别着急,咱们再来分析一下。
首先咱们应该明白next[j]的含义是当匹配失效后,j应该返回模式串的next[j]位置处,也就是前面所说的k位置处。接下来我们分情况讨论:
一开始模式串和主串进行匹配时,如果匹配不成功,由于模式串已经在最开始了,所以主串往前进一步。所以next[0] = -1;
如果模式串和主串在第二个位置匹配失败了呢,由于模式串左边只有一个元素,则只能移动到第一个位置,所以next[1] = 0;
当模式串和主串对应字符匹配成功时,主串和模式串指针均向后前进一步,这个没问题,所以++i,++j;由于i每次不回退,我们仔细想如果前面已经有部分匹配成功,那么对于主串移动过的子串是不是也相当于模式串的后缀呢,而j正好又是模式串移动的步数,是不是前缀呢?这些应该明白了吧,next[++i] == ++j,实际上就是统计模式串前后缀公共的长度。那j= next[j]呢?当模式串和主串在该位置匹配失效后,j返回到next[j]位置处,对指针j进行回退,实际上就是缩小范围找前缀。如果str[i] = str[j],则说明前面的字符也相等,那么对应的公共前后缀长度就是j++;
上面的算法实际上还有一些缺陷,例如
模式串s5 = "aaaab";
主串s6 = "aaabaaaab";
在第四个字符处匹配失败后。会再进行三次多余的a比较,这是因为模式串中前三个字符和模式串的第四个字符相等,所以回退之后,仍然会匹配失效,这是多余的比较。实际上对于这种我们更希望直接回退到不相等的位置。代码如下:
void getnext(string str, vector<int>& next) {
int len = str.size();
next[0] = -1;
int i = 0;
int j = -1;
while (i < len - 1) {
if (j == -1 || str[i] == str[j]) {
i++;
j++;
if (str[i] == str[j]) {
next[i] = next[j];
}
else {
next[i] = j;
}
}
else {
j = next[j];
}
}
}
有了上面的分析之后,KMP算法简而言之就是当模式串回退到初始位置之后,主串的指针前进一步,否则主串位置不变,模式串指针回退到next[j]指定的位置上。下面给出代码:
int KMP(string &str1, string& str2, vector<int> next, int pos) {
int i = pos - 1;
int j = 0;
int len1 = str1.size();
int len2 = str2.size();
while (i < len1 && j < len2)
{
if (j == -1 || str1[i] == str2[j]) {
i++;
j++;
}
else {
j = next[j];
}
}
if (j == str2.size()) {
return i - j + pos - 1;
}
else {
return -1;
}
}
书上讲解的KMP算法由于公式的复杂性,使之看起来十分困难,其实书上讲解的公式就是说明了k是什么,寻找公共前缀和后缀的过程罢了,但实际上KMP算法根本就没有你所想象的那么难。善于画图,自己分析,你会发现其实也并不是那么难!