KMP算法总结
引言
学完KMP算法好长时间了,当初第一次接触时感觉不太好接受。今天我又温习了一遍,重新理了一下KMP算法的思路,结果发现其实并没有那么难,只是有点繁琐。现在我想利用空闲时间根据个人的看法对KMP算法做一下总结。
串是一个线性结构,串匹配就是在主串中定位匹配串第一次出现的位置。
基础串匹配算法
基本思想
主串 “abababcabcabeaba“
匹配串”abcabe“
两个字符串从头开始逐个比较,如果匹配就继续往下比较,如果不匹配则匹配串全体往后移动一位,然后从匹配串的第一位开始再重新比较,直至全部匹配。
事实上该算法每次发现不匹配都需要有重新进行比较,这样每次不匹配时的回溯就大大的提高了时间复杂度。而KMP算法正好巧妙的解决了这一问题。
代码展示
int search(const char *mainstr, const char *matchstr)
{
int mainlen = strlen(mainstr);
int matchlen = strlen(matchstr);
int i = 0;
int j = 0;
if(mainlen <=0 || matchlen <=0 || mainlen < matchlen) //串不符合条件直接返回-1
{
return -1;
}
while(i < mainlen && j < matchlen) //只要没有比较完,就继续比较
{
if (mainstr[i] == matchstr[j]) //如果匹配就继续往后比较
{
i++;
j++;
}else{ //否则重新开始比较
i -= j - 1;
j = 0;
}
}
if (matchstr[j] == 0) //如果匹配串遇到0结束标志,标识匹配出与主串匹配
{
return i - j;
}else{ //否则,则说明不匹配
return -1;
}
}
基础串匹配算法很明显是正确的,但是时间复杂度比较高。如果是最差情况下它的时间复杂度是O(m*n)m表示主串长度,n表示匹配串长度。如果匹配规模较大,则这种算法是很糟糕的!
KMP算法
构建next数组
next数组的构建很巧妙的避免了普通串匹配算法的回溯问题。如果该字符失配,主串停留在该位置不动,移动匹配串,然后进行比较。
next数组含义是失配点前面最长有几个字符与前缀字符串相同,表示如果某字符失配,则下一次该用下标为几的字符比较,简单来说就是作为判断匹配串一次性可以移多少的指标。
例如:ababcabce该串为匹配串,接下来我们来计算该串对应的next数组。(加颜色是为了能够更明确的说明)
代码展示
void GetNext(const char *str, int *next) //传参前用 callloc 申请数组空间,所申请的空间全部为0
{
int i;
int j;
int flag;
for(i = 2; str[i]; i++) //next数组前两个都是0,所以循环直接从2开始
{
flag = 1;
while(flag)
{
if(str[i - 1] == str[j]) //如果失配点前一个字符与该字符对应的next 数组值
{ //为下标所对应的字符相同
next[i] = ++j; //该next数组值在原来的基础上+1
flag = 0; //跳出while循环
}else if(j == 0){ //如果上一个条件不成立且找到字符串与前缀字符串相同时 j = 0
next[i] = j; //直接令该字符所对应的next数组值为 0
flag = 0; //跳出while循环
}else{ //如果 j !=0,直接令 j 等于与之相同的前缀字符的next数组值
j = next[j];
}
}
}
}
还有另外一种实现方式:
public static int[] getNext(String string) {
char[] p = string.toCharArray();
int[] next = new int[p.length];
next[0] = -1;
int j = 0;
int k = -1;
while (j < p.length - 1) {
if (k == -1 || p[j] == p[k]) {
next[++j] = ++k;
} else {
k = next[k];
}
}
return next;
}
KMP
将主串和匹配串逐字符进行比较,如果匹配继续,否则找到失配点,利用构建好的next数组,找到失配点所对应的next数组的值,然后以该值为匹配串的下标,再将主串失配字符与该下标所对应的匹配串的字符比较。(这种方法如果失配,主串里的下标就好停留在那里,不会回溯,这样就避免了主串里将比较完的字符再重新比较)如果(主串长度)mainlen - i (主串下标) + j (匹配串的next数组值) < matchlen(匹配串长度);程序结束,再没必要比较。
代码展示
int Kmp(const char *mainstr, const char *matchstr) //主串和匹配串
{
int mainlen = strlen(mainstr);
int matchlen = strlen(matchstr);
int *next = NULL;
int i = 0;
int j = 0;
if (mainlen <= 0 || matchlen <= 0 || mainlen < matchlen) //输入的串不符合要求
{
return -1;
}
next = (int *) calloc(sizeof(int), matchlen); //用 callloc 申请数组,使申请完空间的值都为 0
if (matchlen > 2)
{
GetNext(matchstr, next); //构建next 数组
}
while(mainlen - i + j >= matchlen) //只要还没有匹配完,就继续。且如果最后的长度小于
{ //匹配串的长度就不要再进行比较了
while(mainstr[i] == matchstr[j] && matchstr[j] != 0) //如果匹配,并且匹配串没完,
{
i++; //继续比较
j++;
}
if (matchstr[j] == 0) //如果匹配串遇到 0 结束标志,说明匹配
{
free(next); //释放申请的空间
return i - matchlen; // 返回匹配起始点的下标
}else if(j == 0){ //如果没有匹配,且 j = 0,进行主串的下一个字符比较
i++;
}else{
j = next[j]; //让主串失配点与匹配串失配点的next 数组值为下标对应的匹配串字符比较
}
}
free(next); //释放申请的空间
return -1; //返回-1 表示主串与匹配串不匹配
}
/*
以上只是核心代码,输出时还需要判断,如果函数返回值为-1 ,说明不匹配;如果返回值为 1 ,
说明匹配。
*/
总结
KMP算法的时间复杂度为O(m + n),m表示主串长度,n表示匹配串长度。
KMP算法是一种改进的字符串匹配算法。关键就是避免了主串的回溯,从而提高了时间复杂度。