前言
在之前的学习中我们曾讲到过库函数strstr
,他的作用是判断一个字符串是否是目标字符串的子串,如果是返回第一次查找到子串的地址,如果没有找到返回NULL。参数是两个字符指针,返回值类型是字符指针。在之前的模拟实现中,我们使用了BF算法,也就是暴力算法,我们觉得这样虽然可以解决问题,但是效率不高,我们想要优化我们的算法,所以今天我要向大家介绍一下KMP算法。
BF算法
首先我们来回顾一下BF算法如何模拟实现strstr
库函数的,我们来看代码。
char* my_strstr(char* str1, const char* str2)
{
assert(str1 && str2);
const char* s1 = str1;
const char* s2 = str2;
const char* p = str1;
if (*s2 == '\0')
{
return NULL;
}
while (*p)
{
s2 = str2;
s1 = p;
while ((*s1 == *s2) && *s1 != '\0' && *s2 != '\0')
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return (char*)p;
}
p++;
}
return NULL;
}
int main()
{
char arr[20] = "adedefgh";
char ch[] = "def";
char* ret = my_strstr(arr, ch);
if (ret == NULL)
{
printf("找不到!!\n");
}
else
{
printf("%s", ret);
}
return 0;
}
我们暴力算法的思路是什么呢?
我们每次查找时,i
向后走,如果找到一样的字符,i++ j++
,如果找到不一样的i j
回到最开始的位置,然后i++
向前走一步。
i
位置与j
位置的字符不相同,所以让i
返回最开始匹配的位置并且i++
,j
返回起点再次比较。
直到源字符串遍历完,说明源字符串是目标字符串的子串。
KMP算法
BF算法虽然可以解决问题,但是有太多不必要的过程。例如:
类似这样的源字符串和子串,我们使用BF算法会浪费很多时间,我们让i++
向后找寻子串,但是当i = 1
时,根本就不可能匹配成功,所以这一步就浪费了时间,再让i++
当i = 2
时,同样不可能匹配成功,又浪费了时间,所以我们需要对我们的算法进行改进。这样我们就引出了KMP算法,我们说BF算法的思想是:让i
回退到开始匹配的位置并让i++
,j
回退到起点。而KMP算法的核心思想是:i
不回退,让j
回退到某个特定的位置。例如:
当我们匹配到i = 5
时发现,两个字符串的字符不相等,这时我们让i
不动,j
回退到一个特定的位置继续匹配。那么j
回退到哪一个位置呢?我们来观察观察
我们看红色区域的字符串跟黄色区域的字符串是否相等,答案是肯定相等的,如果不相等,也不会第一次匹配时匹配到他后面的区域,那黄色区域的字符串跟绿色区域的字符串是不是也是相等的呢。那么我们可以说红色区域内的字符串和绿色区域的的字符串相等,那么我们让j
回退到绿色区域的后一个字符继续匹配是不是就可以了呢?所以当j
在j = 5
时匹配失败,我们让i
不动,j
回退到j = 2
位置处继续匹配,我们将j = 2
称为k值。我们是不是可以设计一个函数求出所有源字符串对应位置匹配失败时j
需要回退到的位置k
,之后我们再将所有k
值放在一个next数组里面,当我们匹配失败时就可以回退到对应位置了。那么该如何取求呢?我们先手动求一下上面这个源字符串的next数组。
我们每次匹配失败,只需要让j
返回到对应k
的位置就可以了,那么我们到底如何求得k
的呢?下面我们来说一说规则:
1.找到匹配成功部分的两个相等的真子串(不包含本身),一个以下标0开始,另一个以j-1下标结尾。
2.不管什么数据next[0] = -1,next[1] = 0,在这里,我们以下标来开始,而说到的第几个第几个是从1开始。
例如:j = 5
时匹配失败,我们寻找两个匹配成功部分的真子串,一个从0下标开始到1下标结束,一个从3下标开始到j - 1
下标结束,长度为2,所以next[5] = 2
。
我们来两个题训练训练吧:
练习一:a b a b c a b c d a b c d e
练习二:a b c a b c a b c a b c d a b c d e
练习一的next数组为:-1 0 0 1 2 0 1 2 0 0 1 2 0 0
练习二的next数组为:-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0
不知道大家算对了么?如果大家算对了,那么说明大家对于next数组如何求解已经掌握了,他的含义已经理解了。那么我们将进行下一步了,已知next[i] = k
如何求next[i+1] = ?
我们举例说明:
如上图,我们已知next[6] = 3
如何推出next[7] = 4
呢?
我们假设next[i] = k
,那么根据我们的规则是不是有这样的一个公式: p[0] -- p[k-1] == p[x] -- p[i -1]
这是因为我们在匹配成功的部分,找到了两个相等的真子串。
既然是真子串那么他们的长度也一定相等,所以k-1 - 0 == i - 1 - x
我们就可推出x = i - k
。
带入到之前的式子中就是p[0] -- p[k-1] == p[i - k] -- p[i -1]
。
如果p[k] == p[i]
。
我们是不是可以得出p[0] -- p[k] == p[i-k] -- p[i]
。
因为p[0] -- p[k-1] == p[i - k] -- p[i -1]
可以推出next[i] = k
。
那么p[0] -- p[k] == p[i-k] -- p[i]
是不是可以推出next[i+1] = k+1
。
所以当next[6] = 3
时,并且p[6] = p[3]
,我们推出了next[7] = 4
。
以上就是我们的推理思路,那么如果p[i] != p[k]
呢?该怎么办呢?我们在举一个例子:
这次我们的p[i] != p[k]
我们该如何处理呢?我们需要继续回退直到p[i] == p[k]
,比如现在k = 2
的位置,而 p[i] != p[k]
,所以我们继续回退,k = 2
时对应的回退位置为0所以我们将k回退到0位置,此时p[i] == p[k]
,所以next[i+1] = k+1
此时i == 5 k == 0
所以next[6] = 1
。
代码实现
在上面我们与大家讲解了有关KMP算法的思路,下面我们使用C语言来实现一下KMP算法
//KMP算法
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
void get_next(char* sub, int* next,int len_sub)
{
next[0] = -1;
next[1] = 0;
int i = 2;//当前的i
int k = 0;//前一项的k
while(i<len_sub)
{
if (k == -1||sub[i - 1] == sub[k])
{
next[i] = k + 1;
i++;
k++;
}
else
{
k = next[k];
}
}
}
int KMP(char* str, char* sub, int pos)//str主串 sub子串 pos代表从主串的pos位置开始找
{
assert(str != NULL && sub != NULL);
int len_str = strlen(str);//主串长度
int len_sub = strlen(sub);//子串长度
if (len_str == 0 || len_sub == 0)
{
return -1;
}
if (pos < 0 || pos >= len_str)
{
return -1;
}
int* next = (int*)malloc(sizeof(int) * len_sub);
assert(next != NULL);
get_next(sub, next,len_sub);
int i = pos;//遍历主串
int j = 0;//遍历子串
while (i < len_str && j < len_sub)
{
if (j == -1 || str[i] == sub[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
if (j >= len_sub)
{
return i - j;
}
else
{
return -1;
}
}
int main()
{
printf("%d\n",KMP("ababcabcdabcde","abcd",0));
return 0;
}
KMP算法的优化
我们来看这个字符串,如果我们在5位置匹配失败,那么我们回退到4位置也一定是失败的,最后一步一步都要回退到-1位置,那么我们可不可以对他进行优化呢?让他一步到位,直接回退到-1位置,这样我们就引出了nextval[]
。
nextval[]
计算规则如下
1.如果回退到的位置和当前字符一样,就写回退那个位置的nextval值。
2.如果回退到的位置和当前字符不一样,就写当前字符原来的next值。
例如:2位置匹配失败,回退到1位置,而2位置是字符'a'
,1位置也是字符'a'
,所以2位置的nextval等于1位置的nextval。7位置匹配失败,回退到6位置,而两个位置的字符不一样,所以7位置的nextval为他自己的next的值。
好了以上就是全部的KMP算法详解,大家可以理解理解,自己尝试着写一写代码,感谢大家的阅读。