KMP算法是用来计算在某个字符串中是否包含另一个字符串的,其实就是leetcode28这种问题,这种问题也可以用暴力法进行解决,但是数据量越大,暴力法用的时间越长,毕竟KMP算法时间复杂度为O(m+n), 而暴力法的时间复杂度为O(m*n)。
首先介绍下暴力法的代码:
#include <iostream>
#include<ctime>
using namespace std;
int strStr(string &haystack, string &needle)
{
int l1=haystack.size();
int l2=needle.size();
if(l2>l1)return -1;
if(l2==0)return 0;
int j=0,i=0;//needle指针
for(int i=0;i<(l1-l2+1);++i)
{
int a=0;
for(int b=0;b<l2;++b)
{
if(haystack[i+a]==needle[a])a++;
if(a==l2)return i;
}
}
return -1;
}
int main() {
string a,b;
a="aaaaaaaaaaas12de";
b="s12de";
clock_t startTime,endTime;
startTime = clock();//计时开始
int res=strStr(a,b);
endTime = clock();//计时结束
cout << "The run time is: " <<(double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;
cout<<res;
return 0;
}
暴力法代码简单易懂,就是从haystack字符串的第一个字符开始比对,看是否有等于needle字符串的子字符串,相应来说时间效率上会低很多,具体过程可以通过下图表示:
时间效率可以通过下面几个例子看出来:
a="zsgbhaiksu+500个a+gyskigas12de" b="s12de"
a="zsgbhaiksu+500个a+gyskigas12de" b="skigas12de"
a="zsgbhaiksu+1500个a+gyskigas12de" b="skigas12de"
由上面两个例子可以看出,当b或者a的长度增加一倍左右时,用时也增加了一倍左右,也证明了暴力法的时间复杂度就是O(m*n)。
暴力法之所以浪费时间,从上面的代码可以看出,每次都要让i进行回溯,这就是浪费时间的原因,KMP算法就可以成功实现i只会往前走,通过调整j的位置进行匹配。
首先看一下KMP算法的代码:
#include <iostream>
#include<ctime>
using namespace std;
void arr_next(vector<int> &next,string p)
{
int pLen = p.size();
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1)
{
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p[j] == p[k])
{
++j;
++k;
//较之前next数组求法,改动在下面4行
if (p[j] != p[k])
next[j] = k;//之前只有这一行
else
//因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
next[j] = next[k];
}
else
{
k = next[k];
}
}
}
int strStr(string haystack, string needle) {
int l1=haystack.size();
int l2=needle.size();
if(l2>l1)
return -1;
if(l2==0)
return 0;
int j=0;//needle指针
int i=0;
vector<int>next(l2,0);
arr_next(next,needle);
while(i<l1&&j<l2)
{
if (j == -1||haystack[i] == needle[j])
{
i++;
j++;
}
else j=next[j];
}
if (j==l2)return i - l2;
return -1;
}
int main() {
string a,b;
a="zsgbhaiks";
b="skigas12de";
clock_t startTime,endTime;
startTime = clock();//计时开始
int res=strStr(a,b);
endTime = clock();//计时结束
cout << "The run time is: " <<(double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;
cout<<res;
return 0;
}
由KMP算法的代码可以看出,对于KMP算法来说,当匹配失败的时候,并不是和上面的暴力法一样让i,j回溯回去,而是让i不变,j往前回溯。
其实原理是这样的,利用与字符串b等长的next数组存储前缀后缀相同的最长长度,举个例子,比如对于字符串abcdabcde来说,对于next[8]来说,在它前面的字符串中相同的前缀字符串和后缀字符串的最大长度为4,因为abcd abcd e,其中第一个abcd为前缀字符串,第二个abcd为后缀字符串,由这个可以判断出,当匹配到e的时候,如果e没有匹配上,那么j往前回溯,回溯到多少呢?就因为第一个abcd和第二个abcd相同,所以只需要回溯到j=4,从abcd abcde开始匹配,因为都能匹配到e,那么说明已经成功匹配的字符串里面肯定是有abcdabcd的,e没有匹配上,可以直接往前回溯到一个最长的相等前后缀。
以字符串abcdabcde举例,其next数组为{-1,0,0,0,0,1,2,3,4},这个版本的next数组还是会有冗余的,其计算原理如下:
对于abcdabcde,它前面没有字符串,所以为-1
对于abcdabcde,它前面没有相同的前缀和后缀字符串,所以为0
对于abcdabcde,同上,所以为0
对于abcdabcde,同上,所以为0
对于abcdabcde,同上,所以为0
对于abcdabcde,前面的两个a相同,最长相同前缀后缀字符串下一个下标为1,所以为1
对于abcdabcde,前缀的ab和后缀的ab相同,所以为2
对于abcdabcde,前缀的abc和后缀的abc相同,所以为3
对于abcdabcde,前缀的abcd和后缀的abcd相同,所以为4
下图就是一个按照next数组进行KMP算法的例子。