KMP算法的个人理解与实现
1. 算法介绍
KMP算法是一种模式匹配算法,什么是模式匹配?简单的理解,就是给定一个主串,以及一个模式串,在主串中匹配查找是否存在模式串并返回具体的位置,举个简单的栗子:
显然,上述模式串在主串中是有匹配的,那么如何通过计算机来实现模式匹配呢?看下面的代码:
int searchStr(char* s, int n, char* t, int m) // s为主串,t为模式串
{
for (int i = 0; i < n; i++)
{
int k = i;
int j = 0;
for (; j < m; j++)
{
if (t[j] == s[k])
{
k++;
}
else
{
break;
}
}
if (j == m) //模式串匹配完成
{
return i;
}
}
return -1;
}
执行:
int main()
{
char s[20];
strcpy_s(s, "adsdcddsgadscsdfge");
char t[5];
strcpy_s(t, "adsc");
int a = searchStr(s, 20, t, 4);
cout << a << endl;
system("pause");
}
这是最暴力的对字符串进行匹配,基本原理就是从第一位开始,用模式串与主串挨个进行比较,如果遇到不匹配的部分,将整个模式串向前移动一个位置,重新比较,直到找到匹配的位置或者主串已经搜索完毕。
很明显,上面的方法在对较小的数据可以使用,当数据量特别大时,执行效率就会很差,而KMP算法就是一种提高了效率的模式匹配算法。
2. 个人理解
按照暴力破解的方式,当模式串移动三次后匹配,会出现下面的情况:
此时,c和f不匹配,我们需要将模式串后移一位,等等!先别急着后移一位,我们观察一下f的前面部分为cabca,是不是发现了什么,这一部分的开头两位和结尾两位都是ca,是不是可以考虑这样移动,即将模式串向后移动三位:
没错!上一步中,我们已经比较过ca了,刚好模式串开头也是ca,显示是不用比较的,可以直接从开头ca后的一位开始比较,这样,就可以有效减少比较次数,提高效率。
那么问题来了,以上是我们通过观察的方法确定移动位置,那么在编码中,我怎么能知道当发现比较不同的位置后,应该从哪一位开始比较呢?
现在,我们已经将问题转化为**当我在模式串的某一个位置跟主串比较时,发现比较失败,我应该重新从模式串的哪一个开始比较?**到这里我们就可以引入KMP算法的next数组了,next数组每个位置的值,就是对应模式串如果在这个位置匹配失败,应该重新从哪个位置匹配的值。
看下面的模式串:
当我们再第五位d匹配失败时,我们应该从第二位b开始重新匹配,因为第五位前面的a和第二位前面的a是一样的,没有必要再匹配,那么对应的next[4]=1;
当我们再最后一位匹配失败时,我们应该从第四位a开始重新匹配,因为第四位前面三位与最后一位的前面三位是一样的,对应next[8]=3。(数组下标从0开始计算)
上面两个例子是我们找某一个位置next值的过程,是用观察法来确定next值,但是用代码的方式如何来实现呢,我们一步一步来实现,先定义求next的方法,需要输入模式串,输出next数组,即:
void getNext(char* str, int n, int* next)
{
}
为了计算每个位置的next值,必然要对每一个位置进行一次遍历:
void getNext(char* str, int n, int* next)
{
int i= 0;
while(i < n)
{
}
}
对于第一个next值,这里特别规定为-1(后面解释原因),就有如下代码:
void getNext(char* str, int n, int* next)
{
int i= 0;
next[0] = -1;
while(i < n)
{
}
}
其他位置的值确定,我们还需要一个辅助的索引,这个索引应该指向的位置是 该位置前面所有的部分构成的子串与当前正在计算next值的位置前面相同长度部分一致。emmm,听起来究极拗口,看下面的图解:
当i指向第六个位置a时,此时辅助索引j应该为i前面部分"ab"与模式串开头"ab"相对应的c的位置,即为j=2,按照上面的步骤,我们先初始化辅助索引,默认为-1(同next[0]后面解释原因):
void getNext(char* str, int n, int* next)
{
next[0] = -1;
int j = -1;
int i= 0;
while(i < n)
{
}
}
现在我们要考虑循环内部怎么实现了,思考一下str[i]和str[j]的值如果相同,代表什么含义?按照i和j的定义,此时j前面的两位应该与i前面的两位相同,那么如果str[i]==str[j],**说明i下一位的next值就是j+1!**即next[++i]=++j:
void getNext(char* str, int n, int* next)
{
int i = 0;
next[0] = -1;
int j = -1;
while (i < n)
{
if (str[i] == str[j])
{
next[++i] = ++j;
}
}
}
既然有相同的判定,必然也有不同的判定,如果不相同呢,next值是重新置为0么?我们再看个例子:
上面例子中,str[i]!=str[j],他们前面相同的部分为abcab,这是我们发现,单看这小块字符串,是不是也有公共部分,因此,当str[i]!=str[j],我们应该让j=next[j],即将j移动至第三位重复上面一步的比较:
此时代码可以如下改动:
void getNext(char* str, int n, int* next)
{
int i = 0;
next[0] = -1;
int j = -1;
while (i < n)
{
if (str[i] == str[j])
{
next[++i] = ++j;
}
else
{
j = next[j];
}
}
}
现在仔细观察我们的代码,发现当i=1时,计算的next[2],当i=2时,计算的是next[3],而next[0]的值我们已经规定为-1,循环只需要到n-2即可停止,同时,我们发现第一次循环时j==-1,需要加一个判定条件,于是:
void getNext(char* str, int n, int* next)
{
int i = 0;
next[0] = -1;
int j = -1;
while (i < n-1) // 这里只需要循环到n-2
{
if (j == -1 || str[i] == str[j]) // 加入对j==-1的判定
{
next[++i] = ++j;
}
else
{
j = next[j];
}
}
}
想象一下next[0]为什么不置为0呢?如果next[0]置为0,当j == 0,str[0] != str[i],执行j=next[j]时,j会变成0,此时会重复执行这个过程陷入死循环,因此我们将next[0]以及j的初始值都设为-1以防止这种情况发生。
以上就是整个next数组的完整求解方法!
到了这一步,我们已经得到了next数组,那么接下来就是利用next数组的比较过程了,同样的,逐步进行分析。首先,匹配方法需要输入两个字符串,并返回最终比较的结果,没有找到返回-1:
int KMP(char* s, int n, char* t, int m)
{
return -1;
}
首先需要计算next数组,并设置两个索引分别遍历主串和模式串,用i遍历主串,用j遍历模式串:
int KMP(char* s, int n, char* t, int m)
{
int* next = (int*)malloc(sizeof(int)*m);
int i = 0, j = 0;
getNext(t, m, next);
while (i < n && j < m)
{
}
free(next);
return -1;
}
当i和j位置的值相同时,自然统一后移一位,当它们的值不同时,将j值置为对应的next值重新比较即可:
int KMP(char* s, int n, char* t, int m)
{
int* next = (int*)malloc(sizeof(int)*m);
int i = 0, j = 0;
getNext(t, m, next);
while (i < n && j < m)
{
// 比较i和j值
if (s[i] == t[j])
{
++i;
++j;
}
else
{
j = next[j];
}
}
free(next);
return -1;
}
最后加上输出和结束判定语句:
int KMP(char* s, int n, char* t, int m)
{
int* next = (int*)malloc(sizeof(int)*m);
int i = 0, j = 0;
getNext(t, m, next);
while (i < n && j < m)
{
if (s[i] == t[j])
{
++i;
++j;
}
else
{
j = next[j];
}
}
free(next);
// 结束判定
if (j >= m)
return i - m;
else
return -1;
}
上述的KMP算法在一些特殊的情况下,可能效率会同样减少,需要对其进行改进,先看一个模式串匹配的例子:
对上述模式串进行next数组求解,我们发现利用该next数组进行比较时,同样会有不必要的比较:
为什么会出现这种情况呢?通过观察,我们发现,next[6] = 3,因此会继续而str[3](蓝色标记)比较,但是str[3]和str[6]的值都是a,str[6]刚才已经经过比较发现不匹配了,很明显用str[3]比较必然也不匹配(因为对应主串的i没有变,都是跟同一个str[i]比较),因此,需要对已有的next数组进行优化,得到新的优化只有的nextval数组,就是改进后的KMP算法。
代码如下,只需要在计算next值的时候再判断一下str[next[j]]与str[j]是否一致:
void getNextVal(char* str, int n, int* nextval, int* next)
{
int i = 0;
next[0] = -1;
next[0] = -1;
int j = -1;
while (i < n - 1) // 这里只需要循环到n-2
{
if (j == -1 || str[i] == str[j]) // 加入对j==-1的判定
{
next[++i] = ++j;
if (str[j] != str[next[j]]) // 不相等,next值与nextval值一致
{
nextval[i] = next[i];
}
else
{
nextval[i] = nextval[next[i]]; // 相等时nextval值为next[i]对应的nextval值,此处使用nextval[next[i]]是因为前面的nextval已经计算过了,是优化过的next值,如果用next[next[i]]则仍然可能有不是最优的可能
}
}
else
{
j = nextval[j]; // 因为前面的nextval已经计算了,所以使用nextval进行回溯
}
}
}
3. 完整代码
-
next数组计算
void getNext(char* str, int n, int* next) { int i = 0; next[0] = -1; int j = -1; while (i < n-1) // 这里只需要循环到n-2 { if (j == -1 || str[i] == str[j]) // 加入对j==-1的判定 { next[++i] = ++j; } else { j = next[j]; } } }
-
nextVal数组计算
void getNextVal(char* str, int n, int* nextval, int* next) { int i = 0; next[0] = -1; next[0] = -1; int j = -1; while (i < n - 1) // 这里只需要循环到n-2 { if (j == -1 || str[i] == str[j]) // 加入对j==-1的判定 { next[++i] = ++j; if (str[j] != str[next[j]]) // 不相等,next值与nextval值一致 { nextval[i] = next[i]; } else { nextval[i] = nextval[next[i]]; // 相等时nextval值为next[i]对应的nextval值 } } else { j = nextval[j]; // 因为前面的nextval已经计算了,所以使用nextval进行回溯 } } }
-
KMP算法
int KMP(char* s, int n, char* t, int m) { int* next = (int*)malloc(sizeof(int)*m); int* nextval = (int*)malloc(sizeof(int)*m); int i = 0, j = 0; getNextVal(t, m, nextval, next); while (i < n && j < m) { if (s[i] == t[j]) { ++i; ++j; } else { j = nextval[j]; } } free(next); free(nextval); if (j >= m) return i - m; else return -1; }
ps:如有错误,请多加指正,转载请注明出处!